Files
gemini-cli/packages/cli/src/ui/components/messages/TopicMessage.test.tsx
T
2026-05-14 13:20:05 -07:00

182 lines
6.1 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { TopicMessage } from './TopicMessage.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import {
TOPIC_PARAM_TITLE,
TOPIC_PARAM_SUMMARY,
TOPIC_PARAM_STRATEGIC_INTENT,
CoreToolCallStatus,
UPDATE_TOPIC_TOOL_NAME,
} from '@google/gemini-cli-core';
import { VirtualizedListContext } from '../shared/VirtualizedList.js';
import { Box, type DOMElement, getBoundingBox } from 'ink';
import { useMouse } from '../../contexts/MouseContext.js';
import { useCallback, useRef } from 'react';
import type React from 'react';
describe('<TopicMessage />', () => {
const baseArgs = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the strategic intent.',
[TOPIC_PARAM_SUMMARY]:
'This is the detailed summary that should be expandable.',
};
const renderTopic = async (
args: Record<string, unknown>,
height?: number,
toolActions?: {
isExpanded?: (callId: string) => boolean;
toggleExpansion?: (callId: string) => void;
},
virtualizedListProps?: {
itemKey?: string;
},
) => {
const defaultItemKey = virtualizedListProps?.itemKey || 'test-topic-key';
const MockVirtualizedListWrapper: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const callbacks = useRef(new Map<string, () => void>());
const mockListContext = {
registerInteractivity: vi.fn(),
setItemState: vi.fn(),
getItemState: vi.fn(),
isItemToggled: vi.fn().mockReturnValue(false),
toggleItem: vi.fn(),
registerClickCallback: vi.fn((key, id, cb) => {
if (key === defaultItemKey) callbacks.current.set(id, cb);
}),
unregisterClickCallback: vi.fn((key, id) => {
if (key === defaultItemKey) callbacks.current.delete(id);
}),
};
const containerRef = useRef<DOMElement>(null);
const handleMouse = useCallback(
(event: { name: string; col: number; row: number }) => {
if (event.name === 'left-press' && containerRef.current) {
const {
x,
y,
width,
height: elHeight,
} = getBoundingBox(containerRef.current);
const mouseX = event.col - 1;
const mouseY = event.row - 1;
if (
mouseX >= x &&
mouseX < x + width &&
mouseY >= y &&
mouseY < y + elHeight
) {
const cb = callbacks.current.get('toggle');
if (cb) cb();
}
}
},
[callbacks],
);
useMouse(handleMouse, { isActive: true });
return (
<VirtualizedListContext.Provider value={mockListContext}>
<Box ref={containerRef}>{children}</Box>
</VirtualizedListContext.Provider>
);
};
return renderWithProviders(
<MockVirtualizedListWrapper>
<TopicMessage
args={args}
itemKey={defaultItemKey}
terminalWidth={80}
availableTerminalHeight={height}
callId="test-topic"
name={UPDATE_TOPIC_TOOL_NAME}
description="Updating topic"
status={CoreToolCallStatus.Success}
confirmationDetails={undefined}
resultDisplay={undefined}
/>
</MockVirtualizedListWrapper>,
{ toolActions, mouseEventsEnabled: true },
);
};
it('renders title and intent by default (collapsed)', async () => {
const { lastFrame } = await renderTopic(baseArgs, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the strategic intent.');
expect(frame).not.toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to expand)');
});
it('renders summary when globally expanded (Ctrl+O)', async () => {
const { lastFrame } = await renderTopic(baseArgs, undefined);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the strategic intent.');
expect(frame).toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to collapse)');
});
it('renders summary when selectively expanded via context', async () => {
const isExpanded = vi.fn((id) => id === 'test-topic');
const { lastFrame } = await renderTopic(baseArgs, 40, { isExpanded });
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to collapse)');
});
it('calls toggleExpansion when clicked', async () => {
const toggleExpansion = vi.fn();
const { simulateClick } = await renderTopic(baseArgs, 40, {
toggleExpansion,
});
// In renderWithProviders, the component is wrapped in a Box with terminalWidth.
// The TopicMessage has marginLeft={2}.
// So col 5 should definitely hit the text content.
// row 1 is the first line of the TopicMessage.
await simulateClick(5, 1);
expect(toggleExpansion).toHaveBeenCalledWith('test-topic');
});
it('falls back to summary if strategic_intent is missing', async () => {
const args = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_SUMMARY]: 'Only summary is present.',
};
const { lastFrame } = await renderTopic(args, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('Only summary is present.');
expect(frame).not.toContain('(ctrl+o to expand)');
});
it('renders only strategic_intent if summary is missing', async () => {
const args = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'Only intent is present.',
};
const { lastFrame } = await renderTopic(args, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('Only intent is present.');
expect(frame).not.toContain('(ctrl+o to expand)');
});
});