mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 10:01:29 -07:00
chore: tests and cleanup
This commit is contained in:
committed by
Keith Guerin
parent
4476c35e4d
commit
57a05b33b5
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { deriveItemsFromLegacySettings } from '../footerItems.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import { deriveItemsFromLegacySettings } from './footerItems.js';
|
||||
import { createMockSettings } from '../test-utils/settings.js';
|
||||
|
||||
describe('deriveItemsFromLegacySettings', () => {
|
||||
it('returns defaults when no legacy settings are customized', () => {
|
||||
@@ -20,7 +20,6 @@ describe('deriveItemsFromLegacySettings', () => {
|
||||
'sandbox-status',
|
||||
'model-name',
|
||||
'quota',
|
||||
'error-count',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -85,7 +84,6 @@ describe('deriveItemsFromLegacySettings', () => {
|
||||
expect(items).toEqual([
|
||||
'git-branch',
|
||||
'sandbox-status',
|
||||
'error-count',
|
||||
'context-remaining',
|
||||
'memory-usage',
|
||||
]);
|
||||
@@ -56,12 +56,6 @@ export const ALL_ITEMS: FooterItem[] = [
|
||||
description: 'Node.js heap memory usage',
|
||||
defaultEnabled: false,
|
||||
},
|
||||
{
|
||||
id: 'error-count',
|
||||
label: 'error-count',
|
||||
description: 'Console errors encountered',
|
||||
defaultEnabled: true,
|
||||
},
|
||||
{
|
||||
id: 'session-id',
|
||||
label: 'session-id',
|
||||
@@ -80,12 +74,6 @@ export const ALL_ITEMS: FooterItem[] = [
|
||||
description: 'Total tokens used in the session',
|
||||
defaultEnabled: false,
|
||||
},
|
||||
{
|
||||
id: 'corgi',
|
||||
label: 'corgi',
|
||||
description: 'A friendly corgi companion',
|
||||
defaultEnabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ORDER = [
|
||||
@@ -96,11 +84,9 @@ export const DEFAULT_ORDER = [
|
||||
'context-remaining',
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'error-count',
|
||||
'session-id',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
'corgi',
|
||||
];
|
||||
|
||||
export function deriveItemsFromLegacySettings(
|
||||
@@ -112,7 +98,6 @@ export function deriveItemsFromLegacySettings(
|
||||
'sandbox-status',
|
||||
'model-name',
|
||||
'quota',
|
||||
'error-count',
|
||||
];
|
||||
const items = [...defaults];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
interface ConsoleSummaryDisplayProps {
|
||||
|
||||
@@ -615,3 +615,206 @@ describe('fallback mode display', () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Footer Token Formatting', () => {
|
||||
const setup = (totalTokens: number) => {
|
||||
const settings = createMockSettings();
|
||||
settings.merged.ui.footer.items = ['token-count'];
|
||||
|
||||
const uiState: { sessionStats: Partial<SessionStatsState> } = {
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 0,
|
||||
sessionId: 'test-session',
|
||||
metrics: {
|
||||
models: {
|
||||
'gemini-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
tokens: {
|
||||
total: totalTokens,
|
||||
input: totalTokens / 2,
|
||||
candidates: totalTokens / 2,
|
||||
prompt: totalTokens / 2,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0, auto_accept: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: { totalLinesAdded: 0, totalLinesRemoved: 0 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return renderWithProviders(<Footer />, {
|
||||
settings,
|
||||
uiState,
|
||||
});
|
||||
};
|
||||
|
||||
it('formats thousands with k', () => {
|
||||
const { lastFrame } = setup(12400);
|
||||
expect(lastFrame()).toContain('12.4k tokens');
|
||||
});
|
||||
|
||||
it('formats millions with m', () => {
|
||||
const { lastFrame } = setup(1500000);
|
||||
expect(lastFrame()).toContain('1.5m tokens');
|
||||
});
|
||||
|
||||
it('formats billions with b', () => {
|
||||
const { lastFrame } = setup(2700000000);
|
||||
expect(lastFrame()).toContain('2.7b tokens');
|
||||
});
|
||||
|
||||
it('formats small numbers without suffix', () => {
|
||||
const { lastFrame } = setup(850);
|
||||
expect(lastFrame()).toContain('850 tokens');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Footer Custom Items', () => {
|
||||
const customMockSessionStats: SessionStatsState = {
|
||||
sessionId: 'test-session-id-12345',
|
||||
sessionStartTime: new Date(),
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 0,
|
||||
metrics: {
|
||||
models: {
|
||||
'gemini-pro': {
|
||||
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
|
||||
tokens: {
|
||||
input: 100,
|
||||
prompt: 0,
|
||||
candidates: 50,
|
||||
total: 150,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 12,
|
||||
totalLinesRemoved: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('renders items in the specified order', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: customMockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['session-id', 'code-changes', 'token-count'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain('test-ses');
|
||||
expect(output).toContain('+12 -4');
|
||||
expect(output).toContain('150 tokens');
|
||||
|
||||
// Check order
|
||||
const idIdx = output!.indexOf('test-ses');
|
||||
const codeIdx = output!.indexOf('+12 -4');
|
||||
const tokenIdx = output!.indexOf('150 tokens');
|
||||
|
||||
expect(idIdx).toBeLessThan(codeIdx);
|
||||
expect(codeIdx).toBeLessThan(tokenIdx);
|
||||
});
|
||||
|
||||
it('renders all items with dividers', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: customMockSessionStats,
|
||||
branchName: 'main',
|
||||
},
|
||||
settings: createMockSettings({
|
||||
general: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['vim-mode', 'cwd', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain('|');
|
||||
expect(output!.split('|').length).toBe(4);
|
||||
});
|
||||
|
||||
it('handles empty items array', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { sessionStats: customMockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output!.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('does not render items that are conditionally hidden', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: customMockSessionStats,
|
||||
branchName: undefined, // No branch
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['cwd', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).not.toContain('('); // Branch is usually in (branch*)
|
||||
expect(output!.split('|').length).toBe(2); // Only cwd and model-name
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,7 +171,12 @@ export const Footer: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
{showMemoryUsage && <MemoryUsageDisplay />}
|
||||
{showMemoryUsage && (
|
||||
<>
|
||||
<Text color={theme.text.secondary}> | </Text>
|
||||
<MemoryUsageDisplay />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box alignItems="center">
|
||||
{corgiMode && (
|
||||
@@ -309,12 +314,6 @@ export const Footer: React.FC = () => {
|
||||
addElement(id, <MemoryUsageDisplay />);
|
||||
break;
|
||||
}
|
||||
case 'error-count': {
|
||||
if (!showErrorDetails && errorCount > 0) {
|
||||
addElement(id, <ConsoleSummaryDisplay errorCount={errorCount} />);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'session-id': {
|
||||
const idShort = uiState.sessionStats.sessionId.slice(0, 8);
|
||||
addElement(id, <Text color={theme.text.secondary}>{idShort}</Text>);
|
||||
@@ -340,28 +339,19 @@ export const Footer: React.FC = () => {
|
||||
totalTokens += m.tokens.total;
|
||||
}
|
||||
if (totalTokens > 0) {
|
||||
const formatted =
|
||||
totalTokens > 1000
|
||||
? `${(totalTokens / 1000).toFixed(1)}k`
|
||||
: totalTokens;
|
||||
let formatted: string;
|
||||
if (totalTokens >= 1_000_000_000) {
|
||||
formatted = `${(totalTokens / 1_000_000_000).toFixed(1)}b`;
|
||||
} else if (totalTokens >= 1_000_000) {
|
||||
formatted = `${(totalTokens / 1_000_000).toFixed(1)}m`;
|
||||
} else if (totalTokens >= 1000) {
|
||||
formatted = `${(totalTokens / 1000).toFixed(1)}k`;
|
||||
} else {
|
||||
formatted = totalTokens.toString();
|
||||
}
|
||||
addElement(
|
||||
id,
|
||||
<Text color={theme.text.secondary}>tokens:{formatted}</Text>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'corgi': {
|
||||
if (corgiMode) {
|
||||
addElement(
|
||||
id,
|
||||
<Text>
|
||||
<Text color={theme.status.error}>▼</Text>
|
||||
<Text color={theme.text.primary}>(´</Text>
|
||||
<Text color={theme.status.error}>ᴥ</Text>
|
||||
<Text color={theme.text.primary}>`)</Text>
|
||||
<Text color={theme.status.error}>▼</Text>
|
||||
</Text>,
|
||||
<Text color={theme.text.secondary}>{formatted} tokens</Text>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -371,6 +361,26 @@ export const Footer: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (corgiMode) {
|
||||
addElement(
|
||||
'corgi-transient',
|
||||
<Text>
|
||||
<Text color={theme.status.error}>▼</Text>
|
||||
<Text color={theme.text.primary}>(´</Text>
|
||||
<Text color={theme.status.error}>ᴥ</Text>
|
||||
<Text color={theme.text.primary}>`)</Text>
|
||||
<Text color={theme.status.error}>▼</Text>
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!showErrorDetails && errorCount > 0) {
|
||||
addElement(
|
||||
'error-count-transient',
|
||||
<ConsoleSummaryDisplay errorCount={errorCount} />,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { FooterConfigDialog } from '../FooterConfigDialog.js';
|
||||
import { createMockSettings } from '../../../test-utils/settings.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { FooterConfigDialog } from './FooterConfigDialog.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import { act } from 'react';
|
||||
|
||||
describe('<FooterConfigDialog />', () => {
|
||||
@@ -123,4 +123,27 @@ describe('<FooterConfigDialog />', () => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('highlights the active item in the preview', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin } = renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
// Initial state: 'cwd' is active.
|
||||
// Verify 'cwd' content exists in the preview area
|
||||
expect(lastFrame()).toContain('~/dev/gemini-cli');
|
||||
|
||||
// Move focus down to 'git-branch'
|
||||
act(() => {
|
||||
stdin.write('\u001b[B'); // Down arrow
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// Verify 'git-branch' content exists in the preview area
|
||||
expect(output).toContain('main*');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,37 +31,53 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
|
||||
// Initialize orderedIds and selectedIds
|
||||
const [orderedIds, setOrderedIds] = useState<string[]>(() => {
|
||||
const validIds = new Set(ALL_ITEMS.map((i) => i.id));
|
||||
|
||||
if (settings.merged.ui?.footer?.items) {
|
||||
// Start with saved items in their saved order
|
||||
const savedItems = settings.merged.ui.footer.items;
|
||||
const savedItems = settings.merged.ui.footer.items.filter((id) =>
|
||||
validIds.has(id),
|
||||
);
|
||||
// Then add any items from DEFAULT_ORDER that aren't in savedItems
|
||||
const others = DEFAULT_ORDER.filter((id) => !savedItems.includes(id));
|
||||
return [...savedItems, ...others];
|
||||
}
|
||||
// Fallback to legacy settings derivation
|
||||
const derived = deriveItemsFromLegacySettings(settings.merged);
|
||||
const derived = deriveItemsFromLegacySettings(settings.merged).filter(
|
||||
(id) => validIds.has(id),
|
||||
);
|
||||
const others = DEFAULT_ORDER.filter((id) => !derived.includes(id));
|
||||
return [...derived, ...others];
|
||||
});
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => {
|
||||
const validIds = new Set(ALL_ITEMS.map((i) => i.id));
|
||||
if (settings.merged.ui?.footer?.items) {
|
||||
return new Set(settings.merged.ui.footer.items);
|
||||
return new Set(
|
||||
settings.merged.ui.footer.items.filter((id) => validIds.has(id)),
|
||||
);
|
||||
}
|
||||
return new Set(deriveItemsFromLegacySettings(settings.merged));
|
||||
return new Set(
|
||||
deriveItemsFromLegacySettings(settings.merged).filter((id) =>
|
||||
validIds.has(id),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Prepare items for fuzzy list
|
||||
const listItems = useMemo(
|
||||
() =>
|
||||
orderedIds.map((id) => {
|
||||
const item = ALL_ITEMS.find((i) => i.id === id)!;
|
||||
return {
|
||||
key: id,
|
||||
label: item.id,
|
||||
description: item.description,
|
||||
};
|
||||
}),
|
||||
orderedIds
|
||||
.map((id) => {
|
||||
const item = ALL_ITEMS.find((i) => i.id === id);
|
||||
if (!item) return null;
|
||||
return {
|
||||
key: id,
|
||||
label: item.id,
|
||||
description: item.description,
|
||||
};
|
||||
})
|
||||
.filter((i): i is NonNullable<typeof i> => i !== null),
|
||||
[orderedIds],
|
||||
);
|
||||
|
||||
@@ -197,35 +213,44 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
scrollOffset + maxItemsToShow,
|
||||
);
|
||||
|
||||
const activeId = filteredItems[activeIndex]?.key;
|
||||
|
||||
// Preview logic
|
||||
const previewText = useMemo(() => {
|
||||
const itemsToPreview = orderedIds.filter((id) => selectedIds.has(id));
|
||||
if (itemsToPreview.length === 0) return 'Empty Footer';
|
||||
|
||||
const getColor = (id: string, defaultColor?: string) =>
|
||||
id === activeId ? 'white' : defaultColor || theme.text.secondary;
|
||||
|
||||
// Mock values for preview
|
||||
const mockValues: Record<string, React.ReactNode> = {
|
||||
cwd: <Text color={theme.text.secondary}>~/dev/gemini-cli</Text>,
|
||||
'git-branch': <Text color={theme.text.secondary}>main*</Text>,
|
||||
'sandbox-status': <Text color="green">macOS Seatbelt</Text>,
|
||||
cwd: <Text color={getColor('cwd')}>~/dev/gemini-cli</Text>,
|
||||
'git-branch': <Text color={getColor('git-branch')}>main*</Text>,
|
||||
'sandbox-status': (
|
||||
<Text color={getColor('sandbox-status', 'green')}>docker</Text>
|
||||
),
|
||||
'model-name': (
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.text.secondary}>gemini-2.5-pro</Text>
|
||||
<Text color={getColor('model-name')}>gemini-2.5-pro</Text>
|
||||
</Box>
|
||||
),
|
||||
'context-remaining': <Text color={theme.text.primary}>85%</Text>,
|
||||
quota: <Text color={theme.text.primary}>1.2k left</Text>,
|
||||
'memory-usage': <Text color={theme.text.primary}>124MB</Text>,
|
||||
'error-count': <Text color={theme.status.error}>2 errors</Text>,
|
||||
'session-id': <Text color={theme.text.secondary}>769992f9</Text>,
|
||||
'context-remaining': (
|
||||
<Text color={getColor('context-remaining')}>85%</Text>
|
||||
),
|
||||
quota: <Text color={getColor('quota')}>1.2k left</Text>,
|
||||
'memory-usage': <Text color={getColor('memory-usage')}>124MB</Text>,
|
||||
'session-id': <Text color={getColor('session-id')}>769992f9</Text>,
|
||||
'code-changes': (
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.status.success}>+12</Text>
|
||||
<Text color={theme.text.primary}> </Text>
|
||||
<Text color={theme.status.error}>-4</Text>
|
||||
<Text color={getColor('code-changes', theme.status.success)}>
|
||||
+12
|
||||
</Text>
|
||||
<Text color={getColor('code-changes')}> </Text>
|
||||
<Text color={getColor('code-changes', theme.status.error)}>-4</Text>
|
||||
</Box>
|
||||
),
|
||||
'token-count': <Text color={theme.text.secondary}>tokens:1.5k</Text>,
|
||||
corgi: <Text>🐶</Text>,
|
||||
'token-count': <Text color={getColor('token-count')}>1.5k tokens</Text>,
|
||||
};
|
||||
|
||||
const elements: React.ReactNode[] = [];
|
||||
@@ -241,7 +266,7 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
});
|
||||
|
||||
return elements;
|
||||
}, [orderedIds, selectedIds]);
|
||||
}, [orderedIds, selectedIds, activeId]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import process from 'node:process';
|
||||
import { formatBytes } from '../utils/formatters.js';
|
||||
@@ -34,7 +34,6 @@ export const MemoryUsageDisplay: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}> | </Text>
|
||||
<Text color={memoryUsageColor}>{memoryUsage}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { createMockSettings } from '../../../test-utils/settings.js';
|
||||
import { Footer } from '../Footer.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
import type { SessionStatsState } from '../../contexts/SessionContext.js';
|
||||
|
||||
const mockSessionStats: SessionStatsState = {
|
||||
sessionId: 'test-session-id-12345',
|
||||
sessionStartTime: new Date(),
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 0,
|
||||
metrics: {
|
||||
models: {
|
||||
'gemini-pro': {
|
||||
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
|
||||
tokens: {
|
||||
input: 100,
|
||||
prompt: 0,
|
||||
candidates: 50,
|
||||
total: 150,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 12,
|
||||
totalLinesRemoved: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Footer Custom Items', () => {
|
||||
it('renders items in the specified order', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['session-id', 'code-changes', 'token-count'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain('test-ses');
|
||||
expect(output).toContain('+12 -4');
|
||||
expect(output).toContain('tokens:150');
|
||||
|
||||
// Check order
|
||||
const idIdx = output!.indexOf('test-ses');
|
||||
const codeIdx = output!.indexOf('+12 -4');
|
||||
const tokenIdx = output!.indexOf('tokens:150');
|
||||
|
||||
expect(idIdx).toBeLessThan(codeIdx);
|
||||
expect(codeIdx).toBeLessThan(tokenIdx);
|
||||
});
|
||||
|
||||
it('renders all items with dividers', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: 'main',
|
||||
},
|
||||
settings: createMockSettings({
|
||||
general: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['vim-mode', 'cwd', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain('|');
|
||||
expect(output!.split('|').length).toBe(4);
|
||||
});
|
||||
|
||||
it('handles empty items array', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output!.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('does not render items that are conditionally hidden', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: undefined, // No branch
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['cwd', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).not.toContain('('); // Branch is usually in (branch*)
|
||||
expect(output!.split('|').length).toBe(2); // Only cwd and model-name
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user