mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 17:11:04 -07:00
fix(cli): enable typechecking for ui/components tests (#11419)
Co-authored-by: Jacob MacDonald <jakemac@google.com>
This commit is contained in:
@@ -31,14 +31,14 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
mcpServers: { 'test-server': { command: 'test' } },
|
||||
ideContext: {
|
||||
workspaceState: {
|
||||
openFiles: [{ path: '/a/b/c' }],
|
||||
openFiles: [{ path: '/a/b/c', timestamp: Date.now() }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should render on a single line on a wide screen', () => {
|
||||
const { lastFrame } = renderWithWidth(120, baseProps);
|
||||
const output = lastFrame();
|
||||
const output = lastFrame()!;
|
||||
expect(output).toContain(
|
||||
'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file | 1 MCP server',
|
||||
);
|
||||
@@ -48,7 +48,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
|
||||
it('should render on multiple lines on a narrow screen', () => {
|
||||
const { lastFrame } = renderWithWidth(60, baseProps);
|
||||
const output = lastFrame();
|
||||
const output = lastFrame()!;
|
||||
const expectedLines = [
|
||||
' Using:',
|
||||
' - 1 open file (ctrl+g to view)',
|
||||
@@ -62,12 +62,12 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
it('should switch layout at the 80-column breakpoint', () => {
|
||||
// At 80 columns, should be on one line
|
||||
const { lastFrame: wideFrame } = renderWithWidth(80, baseProps);
|
||||
expect(wideFrame().includes('\n')).toBe(false);
|
||||
expect(wideFrame()!.includes('\n')).toBe(false);
|
||||
|
||||
// At 79 columns, should be on multiple lines
|
||||
const { lastFrame: narrowFrame } = renderWithWidth(79, baseProps);
|
||||
expect(narrowFrame().includes('\n')).toBe(true);
|
||||
expect(narrowFrame().split('\n').length).toBe(4);
|
||||
expect(narrowFrame()!.includes('\n')).toBe(true);
|
||||
expect(narrowFrame()!.split('\n').length).toBe(4);
|
||||
});
|
||||
|
||||
it('should not render empty parts', () => {
|
||||
@@ -79,7 +79,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
};
|
||||
const { lastFrame } = renderWithWidth(60, props);
|
||||
const expectedLines = [' Using:', ' - 1 open file (ctrl+g to view)'];
|
||||
const actualLines = lastFrame().split('\n');
|
||||
const actualLines = lastFrame()!.split('\n');
|
||||
expect(actualLines).toEqual(expectedLines);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
createMockSettings,
|
||||
} from '../../test-utils/render.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { tildeifyPath } from '@google/gemini-cli-core';
|
||||
import { tildeifyPath, ToolCallDecision } from '@google/gemini-cli-core';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
@@ -32,15 +33,41 @@ const defaultProps = {
|
||||
branchName: 'main',
|
||||
};
|
||||
|
||||
const sessionStats = {
|
||||
sessionStats: { lastPromptTokenCount: 0, lastResponseTokenCount: 0 },
|
||||
const mockSessionStats: SessionStatsState = {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 0,
|
||||
metrics: {
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('<Footer />', () => {
|
||||
it('renders the component', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { branchName: defaultProps.branchName, ...sessionStats },
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
});
|
||||
expect(lastFrame()).toBeDefined();
|
||||
});
|
||||
@@ -49,7 +76,7 @@ describe('<Footer />', () => {
|
||||
it('should display a shortened path on a narrow terminal', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 79,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
const tildePath = tildeifyPath(defaultProps.targetDir);
|
||||
const pathLength = Math.max(20, Math.floor(79 * 0.25));
|
||||
@@ -61,7 +88,7 @@ describe('<Footer />', () => {
|
||||
it('should use wide layout at 80 columns', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 80,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
const tildePath = tildeifyPath(defaultProps.targetDir);
|
||||
const expectedPath =
|
||||
@@ -73,7 +100,10 @@ describe('<Footer />', () => {
|
||||
it('displays the branch name when provided', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { branchName: defaultProps.branchName, ...sessionStats },
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
});
|
||||
expect(lastFrame()).toContain(`(${defaultProps.branchName}*)`);
|
||||
});
|
||||
@@ -81,7 +111,7 @@ describe('<Footer />', () => {
|
||||
it('does not display the branch name when not provided', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { branchName: undefined, ...sessionStats },
|
||||
uiState: { branchName: undefined, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).not.toContain(`(${defaultProps.branchName}*)`);
|
||||
});
|
||||
@@ -89,7 +119,7 @@ describe('<Footer />', () => {
|
||||
it('displays the model name and context percentage', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\(\d+% context left\)/);
|
||||
@@ -98,7 +128,7 @@ describe('<Footer />', () => {
|
||||
it('displays the model name and abbreviated context percentage', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 99,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\(\d+%\)/);
|
||||
@@ -108,7 +138,7 @@ describe('<Footer />', () => {
|
||||
it('should display untrusted when isTrustedFolder is false', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, ...sessionStats },
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
});
|
||||
@@ -117,7 +147,7 @@ describe('<Footer />', () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: undefined, ...sessionStats },
|
||||
uiState: { isTrustedFolder: undefined, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('test');
|
||||
vi.unstubAllEnvs();
|
||||
@@ -128,7 +158,7 @@ describe('<Footer />', () => {
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, ...sessionStats },
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
|
||||
vi.unstubAllEnvs();
|
||||
@@ -139,7 +169,7 @@ describe('<Footer />', () => {
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, ...sessionStats },
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('no sandbox');
|
||||
vi.unstubAllEnvs();
|
||||
@@ -149,7 +179,7 @@ describe('<Footer />', () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, ...sessionStats },
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
expect(lastFrame()).not.toMatch(/test-sandbox/s);
|
||||
@@ -161,7 +191,7 @@ describe('<Footer />', () => {
|
||||
it('renders complete footer with all sections visible (baseline)', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot('complete-footer-wide');
|
||||
});
|
||||
@@ -169,7 +199,7 @@ describe('<Footer />', () => {
|
||||
it('renders footer with all optional sections hidden (minimal footer)', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
@@ -186,7 +216,7 @@ describe('<Footer />', () => {
|
||||
it('renders footer with only model info hidden (partial filtering)', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
@@ -203,7 +233,7 @@ describe('<Footer />', () => {
|
||||
it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
@@ -220,7 +250,7 @@ describe('<Footer />', () => {
|
||||
it('renders complete footer in narrow terminal (baseline narrow)', () => {
|
||||
const { lastFrame } = renderWithProviders(<Footer />, {
|
||||
width: 79,
|
||||
uiState: { ...sessionStats },
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot('complete-footer-narrow');
|
||||
});
|
||||
|
||||
@@ -172,6 +172,9 @@ describe('InputPrompt', () => {
|
||||
text: '',
|
||||
accept: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
isLoading: false,
|
||||
isActive: false,
|
||||
markSelected: vi.fn(),
|
||||
},
|
||||
};
|
||||
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);
|
||||
@@ -200,6 +203,8 @@ describe('InputPrompt', () => {
|
||||
|
||||
mockedUseKittyKeyboardProtocol.mockReturnValue({
|
||||
supported: false,
|
||||
enabled: false,
|
||||
checking: false,
|
||||
});
|
||||
|
||||
props = {
|
||||
@@ -223,6 +228,8 @@ describe('InputPrompt', () => {
|
||||
inputWidth: 80,
|
||||
suggestionsWidth: 80,
|
||||
focus: true,
|
||||
setQueueErrorMessage: vi.fn(),
|
||||
streamingState: StreamingState.Idle,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1553,7 +1560,11 @@ describe('InputPrompt', () => {
|
||||
describe('paste auto-submission protection', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockedUseKittyKeyboardProtocol.mockReturnValue({ supported: false });
|
||||
mockedUseKittyKeyboardProtocol.mockReturnValue({
|
||||
supported: false,
|
||||
enabled: false,
|
||||
checking: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1618,7 +1629,11 @@ describe('InputPrompt', () => {
|
||||
{
|
||||
name: 'kitty',
|
||||
setup: () =>
|
||||
mockedUseKittyKeyboardProtocol.mockReturnValue({ supported: true }),
|
||||
mockedUseKittyKeyboardProtocol.mockReturnValue({
|
||||
supported: true,
|
||||
enabled: true,
|
||||
checking: false,
|
||||
}),
|
||||
},
|
||||
])(
|
||||
'should allow immediate submission for a trusted paste ($name)',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
@@ -24,6 +25,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
@@ -59,9 +61,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -90,9 +101,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
@@ -133,9 +153,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
@@ -176,9 +205,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
@@ -211,9 +249,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -239,9 +286,18 @@ describe('<ModelStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SessionContext>();
|
||||
@@ -23,6 +24,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
@@ -57,7 +59,12 @@ describe('<SessionSummaryDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
@@ -38,23 +39,34 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
return render(<StatsDisplay duration="1s" />);
|
||||
};
|
||||
|
||||
// Helper to create metrics with default zero values
|
||||
const createTestMetrics = (
|
||||
overrides: Partial<SessionMetrics> = {},
|
||||
): SessionMetrics => ({
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('<StatsDisplay />', () => {
|
||||
it('renders only the Performance section in its zero state', () => {
|
||||
const zeroMetrics: SessionMetrics = {
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
const zeroMetrics = createTestMetrics();
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(zeroMetrics);
|
||||
const output = lastFrame();
|
||||
@@ -67,7 +79,7 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
|
||||
it('renders a table with two models correctly', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 3, totalErrors: 0, totalLatencyMs: 15000 },
|
||||
@@ -92,19 +104,7 @@ describe('<StatsDisplay />', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -117,7 +117,7 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
|
||||
it('renders all sections when all data is present', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
@@ -136,22 +136,28 @@ describe('<StatsDisplay />', () => {
|
||||
totalSuccess: 1,
|
||||
totalFail: 1,
|
||||
totalDurationMs: 123,
|
||||
totalDecisions: { accept: 1, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 1,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {
|
||||
'test-tool': {
|
||||
count: 2,
|
||||
success: 1,
|
||||
fail: 1,
|
||||
durationMs: 123,
|
||||
decisions: { accept: 1, reject: 0, modify: 0 },
|
||||
decisions: {
|
||||
accept: 1,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -166,29 +172,34 @@ describe('<StatsDisplay />', () => {
|
||||
|
||||
describe('Conditional Rendering Tests', () => {
|
||||
it('hides User Agreement when no decisions are made', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 2,
|
||||
totalSuccess: 1,
|
||||
totalFail: 1,
|
||||
totalDurationMs: 123,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 }, // No decisions
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
}, // No decisions
|
||||
byName: {
|
||||
'test-tool': {
|
||||
count: 2,
|
||||
success: 1,
|
||||
fail: 1,
|
||||
durationMs: 123,
|
||||
decisions: { accept: 0, reject: 0, modify: 0 },
|
||||
decisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -200,7 +211,7 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
|
||||
it('hides Efficiency section when cache is not used', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
@@ -214,19 +225,7 @@ describe('<StatsDisplay />', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -238,61 +237,61 @@ describe('<StatsDisplay />', () => {
|
||||
|
||||
describe('Conditional Color Tests', () => {
|
||||
it('renders success rate in green for high values', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 10,
|
||||
totalSuccess: 10,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders success rate in yellow for medium values', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 10,
|
||||
totalSuccess: 9,
|
||||
totalFail: 1,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders success rate in red for low values', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 10,
|
||||
totalSuccess: 5,
|
||||
totalFail: 5,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
@@ -300,21 +299,25 @@ describe('<StatsDisplay />', () => {
|
||||
|
||||
describe('Code Changes Display', () => {
|
||||
it('displays Code Changes when line counts are present', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 1,
|
||||
totalSuccess: 1,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 100,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 42,
|
||||
totalLinesRemoved: 18,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -326,21 +329,21 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
|
||||
it('hides Code Changes when no lines are added or removed', () => {
|
||||
const metrics: SessionMetrics = {
|
||||
models: {},
|
||||
const metrics = createTestMetrics({
|
||||
tools: {
|
||||
totalCalls: 1,
|
||||
totalSuccess: 1,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 100,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
@@ -351,21 +354,7 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
|
||||
describe('Title Rendering', () => {
|
||||
const zeroMetrics: SessionMetrics = {
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
const zeroMetrics = createTestMetrics();
|
||||
|
||||
it('renders the default title when no title prop is provided', () => {
|
||||
const { lastFrame } = renderWithMockedStats(zeroMetrics);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
@@ -24,6 +25,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
@@ -46,9 +48,18 @@ describe('<ToolStatsDisplay />', () => {
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -65,17 +76,31 @@ describe('<ToolStatsDisplay />', () => {
|
||||
totalSuccess: 1,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 100,
|
||||
totalDecisions: { accept: 1, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 1,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {
|
||||
'test-tool': {
|
||||
count: 1,
|
||||
success: 1,
|
||||
fail: 0,
|
||||
durationMs: 100,
|
||||
decisions: { accept: 1, reject: 0, modify: 0 },
|
||||
decisions: {
|
||||
accept: 1,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
@@ -91,24 +116,43 @@ describe('<ToolStatsDisplay />', () => {
|
||||
totalSuccess: 2,
|
||||
totalFail: 1,
|
||||
totalDurationMs: 300,
|
||||
totalDecisions: { accept: 1, reject: 1, modify: 1 },
|
||||
totalDecisions: {
|
||||
accept: 1,
|
||||
reject: 1,
|
||||
modify: 1,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {
|
||||
'tool-a': {
|
||||
count: 2,
|
||||
success: 1,
|
||||
fail: 1,
|
||||
durationMs: 200,
|
||||
decisions: { accept: 1, reject: 1, modify: 0 },
|
||||
decisions: {
|
||||
accept: 1,
|
||||
reject: 1,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
'tool-b': {
|
||||
count: 1,
|
||||
success: 1,
|
||||
fail: 0,
|
||||
durationMs: 100,
|
||||
decisions: { accept: 0, reject: 0, modify: 1 },
|
||||
decisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 1,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
@@ -129,6 +173,7 @@ describe('<ToolStatsDisplay />', () => {
|
||||
accept: 123456789,
|
||||
reject: 98765432,
|
||||
modify: 12345,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {
|
||||
'long-named-tool-for-testing-wrapping-and-such': {
|
||||
@@ -140,10 +185,15 @@ describe('<ToolStatsDisplay />', () => {
|
||||
accept: 123456789,
|
||||
reject: 98765432,
|
||||
modify: 12345,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -157,17 +207,31 @@ describe('<ToolStatsDisplay />', () => {
|
||||
totalSuccess: 1,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 100,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {
|
||||
'test-tool': {
|
||||
count: 1,
|
||||
success: 1,
|
||||
fail: 0,
|
||||
durationMs: 100,
|
||||
decisions: { accept: 0, reject: 0, modify: 0 },
|
||||
decisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
|
||||
│ Agent powering down. Goodbye! │
|
||||
│ │
|
||||
│ Interaction Summary │
|
||||
│ Session ID: │
|
||||
│ Session ID: test-session │
|
||||
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
|
||||
│ Success Rate: 0.0% │
|
||||
│ Code Changes: +42 -15 │
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
TextBuffer,
|
||||
TextBufferState,
|
||||
TextBufferAction,
|
||||
VisualLayout,
|
||||
} from './text-buffer.js';
|
||||
import {
|
||||
useTextBuffer,
|
||||
@@ -24,6 +25,12 @@ import {
|
||||
} from './text-buffer.js';
|
||||
import { cpLen } from '../../utils/textUtils.js';
|
||||
|
||||
const defaultVisualLayout: VisualLayout = {
|
||||
visualLines: [''],
|
||||
logicalToVisualMap: [[[0, 0]]],
|
||||
visualToLogicalMap: [[0, 0]],
|
||||
};
|
||||
|
||||
const initialState: TextBufferState = {
|
||||
lines: [''],
|
||||
cursorRow: 0,
|
||||
@@ -33,6 +40,9 @@ const initialState: TextBufferState = {
|
||||
redoStack: [],
|
||||
clipboard: null,
|
||||
selectionAnchor: null,
|
||||
viewportWidth: 80,
|
||||
viewportHeight: 24,
|
||||
visualLayout: defaultVisualLayout,
|
||||
};
|
||||
|
||||
describe('textBufferReducer', () => {
|
||||
@@ -1700,28 +1710,40 @@ describe('logicalPosToOffset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to create state for reducer tests
|
||||
const createTestState = (
|
||||
lines: string[],
|
||||
cursorRow: number,
|
||||
cursorCol: number,
|
||||
viewportWidth = 80,
|
||||
): TextBufferState => {
|
||||
const text = lines.join('\n');
|
||||
let state = textBufferReducer(initialState, {
|
||||
type: 'set_text',
|
||||
payload: text,
|
||||
});
|
||||
state = textBufferReducer(state, {
|
||||
type: 'set_cursor',
|
||||
payload: { cursorRow, cursorCol, preferredCol: null },
|
||||
});
|
||||
state = textBufferReducer(state, {
|
||||
type: 'set_viewport',
|
||||
payload: { width: viewportWidth, height: 24 },
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
describe('textBufferReducer vim operations', () => {
|
||||
describe('vim_delete_line', () => {
|
||||
it('should delete a single line including newline in multi-line text', () => {
|
||||
const initialState: TextBufferState = {
|
||||
lines: ['line1', 'line2', 'line3'],
|
||||
cursorRow: 1,
|
||||
cursorCol: 2,
|
||||
preferredCol: null,
|
||||
visualLines: [['line1'], ['line2'], ['line3']],
|
||||
visualScrollRow: 0,
|
||||
visualCursor: { row: 1, col: 2 },
|
||||
viewport: { width: 10, height: 5 },
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
const state = createTestState(['line1', 'line2', 'line3'], 1, 2);
|
||||
|
||||
const action: TextBufferAction = {
|
||||
type: 'vim_delete_line',
|
||||
payload: { count: 1 },
|
||||
};
|
||||
|
||||
const result = textBufferReducer(initialState, action);
|
||||
const result = textBufferReducer(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
|
||||
// After deleting line2, we should have line1 and line3, with cursor on line3 (now at index 1)
|
||||
@@ -1731,25 +1753,14 @@ describe('textBufferReducer vim operations', () => {
|
||||
});
|
||||
|
||||
it('should delete multiple lines when count > 1', () => {
|
||||
const initialState: TextBufferState = {
|
||||
lines: ['line1', 'line2', 'line3', 'line4'],
|
||||
cursorRow: 1,
|
||||
cursorCol: 0,
|
||||
preferredCol: null,
|
||||
visualLines: [['line1'], ['line2'], ['line3'], ['line4']],
|
||||
visualScrollRow: 0,
|
||||
visualCursor: { row: 1, col: 0 },
|
||||
viewport: { width: 10, height: 5 },
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
const state = createTestState(['line1', 'line2', 'line3', 'line4'], 1, 0);
|
||||
|
||||
const action: TextBufferAction = {
|
||||
type: 'vim_delete_line',
|
||||
payload: { count: 2 },
|
||||
};
|
||||
|
||||
const result = textBufferReducer(initialState, action);
|
||||
const result = textBufferReducer(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
|
||||
// Should delete line2 and line3, leaving line1 and line4
|
||||
@@ -1759,25 +1770,14 @@ describe('textBufferReducer vim operations', () => {
|
||||
});
|
||||
|
||||
it('should clear single line content when only one line exists', () => {
|
||||
const initialState: TextBufferState = {
|
||||
lines: ['only line'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 5,
|
||||
preferredCol: null,
|
||||
visualLines: [['only line']],
|
||||
visualScrollRow: 0,
|
||||
visualCursor: { row: 0, col: 5 },
|
||||
viewport: { width: 10, height: 5 },
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
const state = createTestState(['only line'], 0, 5);
|
||||
|
||||
const action: TextBufferAction = {
|
||||
type: 'vim_delete_line',
|
||||
payload: { count: 1 },
|
||||
};
|
||||
|
||||
const result = textBufferReducer(initialState, action);
|
||||
const result = textBufferReducer(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
|
||||
// Should clear the line content but keep the line
|
||||
@@ -1787,25 +1787,14 @@ describe('textBufferReducer vim operations', () => {
|
||||
});
|
||||
|
||||
it('should handle deleting the last line properly', () => {
|
||||
const initialState: TextBufferState = {
|
||||
lines: ['line1', 'line2'],
|
||||
cursorRow: 1,
|
||||
cursorCol: 0,
|
||||
preferredCol: null,
|
||||
visualLines: [['line1'], ['line2']],
|
||||
visualScrollRow: 0,
|
||||
visualCursor: { row: 1, col: 0 },
|
||||
viewport: { width: 10, height: 5 },
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
const state = createTestState(['line1', 'line2'], 1, 0);
|
||||
|
||||
const action: TextBufferAction = {
|
||||
type: 'vim_delete_line',
|
||||
payload: { count: 1 },
|
||||
};
|
||||
|
||||
const result = textBufferReducer(initialState, action);
|
||||
const result = textBufferReducer(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
|
||||
// Should delete the last line completely, not leave empty line
|
||||
@@ -1815,18 +1804,7 @@ describe('textBufferReducer vim operations', () => {
|
||||
});
|
||||
|
||||
it('should handle deleting all lines and maintain valid state for subsequent paste', () => {
|
||||
const initialState: TextBufferState = {
|
||||
lines: ['line1', 'line2', 'line3', 'line4'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
preferredCol: null,
|
||||
visualLines: [['line1'], ['line2'], ['line3'], ['line4']],
|
||||
visualScrollRow: 0,
|
||||
visualCursor: { row: 0, col: 0 },
|
||||
viewport: { width: 10, height: 5 },
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
const state = createTestState(['line1', 'line2', 'line3', 'line4'], 0, 0);
|
||||
|
||||
// Delete all 4 lines with 4dd
|
||||
const deleteAction: TextBufferAction = {
|
||||
@@ -1834,7 +1812,7 @@ describe('textBufferReducer vim operations', () => {
|
||||
payload: { count: 4 },
|
||||
};
|
||||
|
||||
const afterDelete = textBufferReducer(initialState, deleteAction);
|
||||
const afterDelete = textBufferReducer(state, deleteAction);
|
||||
expect(afterDelete).toHaveOnlyValidCharacters();
|
||||
|
||||
// After deleting all lines, should have one empty line
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { handleVimAction } from './vim-buffer-actions.js';
|
||||
import type { TextBufferState } from './text-buffer.js';
|
||||
import type { TextBufferState, VisualLayout } from './text-buffer.js';
|
||||
|
||||
const defaultVisualLayout: VisualLayout = {
|
||||
visualLines: [''],
|
||||
logicalToVisualMap: [[[0, 0]]],
|
||||
visualToLogicalMap: [[0, 0]],
|
||||
};
|
||||
|
||||
// Helper to create test state
|
||||
const createTestState = (
|
||||
@@ -23,6 +29,8 @@ const createTestState = (
|
||||
clipboard: null,
|
||||
selectionAnchor: null,
|
||||
viewportWidth: 80,
|
||||
viewportHeight: 24,
|
||||
visualLayout: defaultVisualLayout,
|
||||
});
|
||||
|
||||
describe('vim-buffer-actions', () => {
|
||||
@@ -266,7 +274,7 @@ describe('vim-buffer-actions', () => {
|
||||
|
||||
const result = handleVimAction(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
expect(result.cursorRow).toBe(1); // Should move to empty line
|
||||
expect(result.cursorRow).toBe(1);
|
||||
expect(result.cursorCol).toBe(0); // Beginning of empty line
|
||||
});
|
||||
});
|
||||
@@ -331,7 +339,7 @@ describe('vim-buffer-actions', () => {
|
||||
|
||||
const result = handleVimAction(state, action);
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
expect(result.cursorRow).toBe(2); // Should move to line with 'test'
|
||||
expect(result.cursorRow).toBe(2);
|
||||
expect(result.cursorCol).toBe(3); // Should be at 't' (end of 'test')
|
||||
});
|
||||
|
||||
@@ -354,7 +362,7 @@ describe('vim-buffer-actions', () => {
|
||||
payload: { count: 1 },
|
||||
});
|
||||
expect(result).toHaveOnlyValidCharacters();
|
||||
expect(result.cursorRow).toBe(1); // Should move to empty line
|
||||
expect(result.cursorRow).toBe(1);
|
||||
expect(result.cursorCol).toBe(0); // Empty line has col 0
|
||||
});
|
||||
|
||||
@@ -382,7 +390,7 @@ describe('vim-buffer-actions', () => {
|
||||
|
||||
it('should handle precomposed characters with diacritics', () => {
|
||||
// Test case with precomposed é for comparison
|
||||
const state = createTestState(['café test'], 0, 0); // Start at 'c'
|
||||
const state = createTestState(['café test'], 0, 0);
|
||||
|
||||
// First 'e' command should move to the 'é' (position 3)
|
||||
let result = handleVimAction(state, {
|
||||
@@ -814,7 +822,7 @@ describe('vim-buffer-actions', () => {
|
||||
const state = createTestState(['hello world'], 0, 5);
|
||||
const action = {
|
||||
type: 'vim_change_movement' as const,
|
||||
payload: { movement: 'h', count: 2 },
|
||||
payload: { movement: 'h' as const, count: 2 },
|
||||
};
|
||||
|
||||
const result = handleVimAction(state, action);
|
||||
@@ -827,7 +835,7 @@ describe('vim-buffer-actions', () => {
|
||||
const state = createTestState(['hello world'], 0, 5);
|
||||
const action = {
|
||||
type: 'vim_change_movement' as const,
|
||||
payload: { movement: 'l', count: 3 },
|
||||
payload: { movement: 'l' as const, count: 3 },
|
||||
};
|
||||
|
||||
const result = handleVimAction(state, action);
|
||||
@@ -840,7 +848,7 @@ describe('vim-buffer-actions', () => {
|
||||
const state = createTestState(['line1', 'line2', 'line3'], 0, 2);
|
||||
const action = {
|
||||
type: 'vim_change_movement' as const,
|
||||
payload: { movement: 'j', count: 2 },
|
||||
payload: { movement: 'j' as const, count: 2 },
|
||||
};
|
||||
|
||||
const result = handleVimAction(state, action);
|
||||
|
||||
Reference in New Issue
Block a user