mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-05 19:01:12 -07:00
Improve test coverage for cli/src/ui/components (#13598)
This commit is contained in:
57
packages/cli/src/ui/components/AboutBox.test.tsx
Normal file
57
packages/cli/src/ui/components/AboutBox.test.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock GIT_COMMIT_INFO
|
||||
vi.mock('../../generated/git-commit.js', () => ({
|
||||
GIT_COMMIT_INFO: 'mock-commit-hash',
|
||||
}));
|
||||
|
||||
describe('AboutBox', () => {
|
||||
const defaultProps = {
|
||||
cliVersion: '1.0.0',
|
||||
osVersion: 'macOS',
|
||||
sandboxEnv: 'default',
|
||||
modelVersion: 'gemini-pro',
|
||||
selectedAuthType: 'oauth',
|
||||
gcpProject: '',
|
||||
ideClient: '',
|
||||
};
|
||||
|
||||
it('renders with required props', () => {
|
||||
const { lastFrame } = render(<AboutBox {...defaultProps} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('About Gemini CLI');
|
||||
expect(output).toContain('1.0.0');
|
||||
expect(output).toContain('mock-commit-hash');
|
||||
expect(output).toContain('gemini-pro');
|
||||
expect(output).toContain('default');
|
||||
expect(output).toContain('macOS');
|
||||
expect(output).toContain('OAuth');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['userEmail', 'test@example.com', 'User Email'],
|
||||
['gcpProject', 'my-project', 'GCP Project'],
|
||||
['ideClient', 'vscode', 'IDE Client'],
|
||||
])('renders optional prop %s', (prop, value, label) => {
|
||||
const props = { ...defaultProps, [prop]: value };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(label);
|
||||
expect(output).toContain(value);
|
||||
});
|
||||
|
||||
it('renders Auth Method correctly when not oauth', () => {
|
||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
});
|
||||
});
|
||||
39
packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx
Normal file
39
packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
describe('AutoAcceptIndicator', () => {
|
||||
it('renders correctly for AUTO_EDIT mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('accepting edits');
|
||||
expect(output).toContain('(shift + tab to toggle)');
|
||||
});
|
||||
|
||||
it('renders correctly for YOLO mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<AutoAcceptIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('YOLO mode');
|
||||
expect(output).toContain('(ctrl + y to toggle)');
|
||||
});
|
||||
|
||||
it('renders nothing for DEFAULT mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<AutoAcceptIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('accepting edits');
|
||||
expect(output).not.toContain('YOLO mode');
|
||||
});
|
||||
});
|
||||
29
packages/cli/src/ui/components/Banner.test.tsx
Normal file
29
packages/cli/src/ui/components/Banner.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Banner } from './Banner.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Banner', () => {
|
||||
it.each([
|
||||
['warning mode', true, 'Warning Message'],
|
||||
['info mode', false, 'Info Message'],
|
||||
])('renders in %s', (_, isWarning, text) => {
|
||||
const { lastFrame } = render(
|
||||
<Banner bannerText={text} isWarning={isWarning} width={80} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles newlines in text', () => {
|
||||
const text = 'Line 1\\nLine 2';
|
||||
const { lastFrame } = render(
|
||||
<Banner bannerText={text} isWarning={false} width={80} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
117
packages/cli/src/ui/components/ConfigInitDisplay.test.tsx
Normal file
117
packages/cli/src/ui/components/ConfigInitDisplay.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { AppEvent } from '../../utils/events.js';
|
||||
import { MCPServerStatus, type McpClient } from '@google/gemini-cli-core';
|
||||
import { Text } from 'ink';
|
||||
|
||||
// Mock GeminiSpinner
|
||||
vi.mock('./GeminiRespondingSpinner.js', () => ({
|
||||
GeminiSpinner: () => <Text>Spinner</Text>,
|
||||
}));
|
||||
|
||||
// Mock appEvents
|
||||
const { mockOn, mockOff, mockEmit } = vi.hoisted(() => ({
|
||||
mockOn: vi.fn(),
|
||||
mockOff: vi.fn(),
|
||||
mockEmit: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/events.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../utils/events.js')>();
|
||||
return {
|
||||
...actual,
|
||||
appEvents: {
|
||||
on: mockOn,
|
||||
off: mockOff,
|
||||
emit: mockEmit,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('ConfigInitDisplay', () => {
|
||||
beforeEach(() => {
|
||||
mockOn.mockClear();
|
||||
mockOff.mockClear();
|
||||
mockEmit.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders initial state', () => {
|
||||
const { lastFrame } = render(<ConfigInitDisplay />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('updates message on McpClientUpdate event', async () => {
|
||||
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
|
||||
mockOn.mockImplementation((event, fn) => {
|
||||
if (event === AppEvent.McpClientUpdate) {
|
||||
listener = fn;
|
||||
}
|
||||
});
|
||||
|
||||
const { lastFrame } = render(<ConfigInitDisplay />);
|
||||
|
||||
// Wait for listener to be registered
|
||||
await vi.waitFor(() => {
|
||||
if (!listener) throw new Error('Listener not registered yet');
|
||||
});
|
||||
|
||||
const mockClient1 = {
|
||||
getStatus: () => MCPServerStatus.CONNECTED,
|
||||
} as McpClient;
|
||||
const mockClient2 = {
|
||||
getStatus: () => MCPServerStatus.CONNECTING,
|
||||
} as McpClient;
|
||||
const clients = new Map<string, McpClient>([
|
||||
['server1', mockClient1],
|
||||
['server2', mockClient2],
|
||||
]);
|
||||
|
||||
// Trigger the listener manually since we mocked the event emitter
|
||||
act(() => {
|
||||
listener!(clients);
|
||||
});
|
||||
|
||||
// Wait for the UI to update
|
||||
await vi.waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty clients map', async () => {
|
||||
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
|
||||
mockOn.mockImplementation((event, fn) => {
|
||||
if (event === AppEvent.McpClientUpdate) {
|
||||
listener = fn;
|
||||
}
|
||||
});
|
||||
|
||||
const { lastFrame } = render(<ConfigInitDisplay />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
if (!listener) throw new Error('Listener not registered yet');
|
||||
});
|
||||
|
||||
if (listener) {
|
||||
const safeListener = listener;
|
||||
act(() => {
|
||||
safeListener(new Map());
|
||||
});
|
||||
}
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ConsoleSummaryDisplay', () => {
|
||||
it('renders nothing when errorCount is 0', () => {
|
||||
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={0} />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1, '1 error'],
|
||||
[5, '5 errors'],
|
||||
])('renders correct message for %i errors', (count, expectedText) => {
|
||||
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={count} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(expectedText);
|
||||
expect(output).toContain('✖');
|
||||
expect(output).toContain('(F12 for details)');
|
||||
});
|
||||
});
|
||||
61
packages/cli/src/ui/components/ContextUsageDisplay.test.tsx
Normal file
61
packages/cli/src/ui/components/ContextUsageDisplay.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
tokenLimit: () => 10000,
|
||||
}));
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
DEFAULT_MODEL_CONFIGS: {},
|
||||
LoadedSettings: class {
|
||||
constructor() {
|
||||
// this.merged = {};
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ContextUsageDisplay', () => {
|
||||
it('renders correct percentage left', () => {
|
||||
const { lastFrame } = render(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={5000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('50% context left');
|
||||
});
|
||||
|
||||
it('renders short label when terminal width is small', () => {
|
||||
const { lastFrame } = render(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={2000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('80%');
|
||||
expect(output).not.toContain('context left');
|
||||
});
|
||||
|
||||
it('renders 0% when full', () => {
|
||||
const { lastFrame } = render(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={10000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('0% context left');
|
||||
});
|
||||
});
|
||||
37
packages/cli/src/ui/components/CopyModeWarning.test.tsx
Normal file
37
packages/cli/src/ui/components/CopyModeWarning.test.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { CopyModeWarning } from './CopyModeWarning.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
|
||||
describe('CopyModeWarning', () => {
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when copy mode is disabled', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
copyModeEnabled: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<CopyModeWarning />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders warning when copy mode is enabled', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
copyModeEnabled: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<CopyModeWarning />);
|
||||
expect(lastFrame()).toContain('In Copy Mode');
|
||||
expect(lastFrame()).toContain('Press any key to exit');
|
||||
});
|
||||
});
|
||||
@@ -8,12 +8,19 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||
import {
|
||||
profiler,
|
||||
DebugProfiler,
|
||||
ACTION_TIMESTAMP_CAPACITY,
|
||||
FRAME_TIMESTAMP_CAPACITY,
|
||||
} from './DebugProfiler.js';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
import { FixedDeque } from 'mnemonist';
|
||||
import { debugState } from '../debug.js';
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('DebugProfiler', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -214,3 +221,49 @@ describe('DebugProfiler', () => {
|
||||
expect(profiler.totalIdleFrames).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DebugProfiler Component', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the mock implementation before each test
|
||||
vi.mocked(useUIState).mockReturnValue({
|
||||
showDebugProfiler: false,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
|
||||
// Mock process.stdin and stdout
|
||||
// We need to be careful not to break the test runner's own output
|
||||
// So we might want to skip mocking them if they are not strictly needed for the simple render test
|
||||
// or mock them safely.
|
||||
// For now, let's assume the component uses them in useEffect.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return null when showDebugProfiler is false', () => {
|
||||
vi.mocked(useUIState).mockReturnValue({
|
||||
showDebugProfiler: false,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<DebugProfiler />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('should render stats when showDebugProfiler is true', () => {
|
||||
vi.mocked(useUIState).mockReturnValue({
|
||||
showDebugProfiler: true,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
profiler.numFrames = 10;
|
||||
profiler.totalIdleFrames = 5;
|
||||
profiler.totalFlickerFrames = 2;
|
||||
|
||||
const { lastFrame } = render(<DebugProfiler />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Renders: 10 (total)');
|
||||
expect(output).toContain('5 (idle)');
|
||||
expect(output).toContain('2 (flicker)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
import { Box } from 'ink';
|
||||
import type React from 'react';
|
||||
|
||||
vi.mock('./shared/ScrollableList.js', () => ({
|
||||
ScrollableList: ({
|
||||
data,
|
||||
renderItem,
|
||||
}: {
|
||||
data: unknown[];
|
||||
renderItem: (props: { item: unknown }) => React.ReactNode;
|
||||
}) => (
|
||||
<Box flexDirection="column">
|
||||
{data.map((item: unknown, index: number) => (
|
||||
<Box key={index}>{renderItem({ item })}</Box>
|
||||
))}
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('DetailedMessagesDisplay', () => {
|
||||
it('renders nothing when messages are empty', () => {
|
||||
const { lastFrame } = render(
|
||||
<DetailedMessagesDisplay
|
||||
messages={[]}
|
||||
maxHeight={10}
|
||||
width={80}
|
||||
hasFocus={false}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders messages correctly', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'log', content: 'Log message', count: 1 },
|
||||
{ type: 'warn', content: 'Warning message', count: 1 },
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
{ type: 'debug', content: 'Debug message', count: 1 },
|
||||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<DetailedMessagesDisplay
|
||||
messages={messages}
|
||||
maxHeight={20}
|
||||
width={80}
|
||||
hasFocus={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Debug Console');
|
||||
expect(output).toContain('Log message');
|
||||
expect(output).toContain('Warning message');
|
||||
expect(output).toContain('Error message');
|
||||
expect(output).toContain('Debug message');
|
||||
|
||||
// Check for icons
|
||||
expect(output).toContain('ℹ');
|
||||
expect(output).toContain('⚠');
|
||||
expect(output).toContain('✖');
|
||||
expect(output).toContain('🔍');
|
||||
});
|
||||
|
||||
it('renders message counts', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'log', content: 'Repeated message', count: 5 },
|
||||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<DetailedMessagesDisplay
|
||||
messages={messages}
|
||||
maxHeight={10}
|
||||
width={80}
|
||||
hasFocus={false}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Repeated message');
|
||||
expect(output).toContain('(x5)');
|
||||
});
|
||||
});
|
||||
180
packages/cli/src/ui/components/DialogManager.test.tsx
Normal file
180
packages/cli/src/ui/components/DialogManager.test.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { DialogManager } from './DialogManager.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import { type UIState } from '../contexts/UIStateContext.js';
|
||||
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
||||
import { type IdeInfo } from '@google/gemini-cli-core';
|
||||
import { type ShellConfirmationRequest } from '../types.js';
|
||||
|
||||
// Mock child components
|
||||
vi.mock('../IdeIntegrationNudge.js', () => ({
|
||||
IdeIntegrationNudge: () => <Text>IdeIntegrationNudge</Text>,
|
||||
}));
|
||||
vi.mock('./LoopDetectionConfirmation.js', () => ({
|
||||
LoopDetectionConfirmation: () => <Text>LoopDetectionConfirmation</Text>,
|
||||
}));
|
||||
vi.mock('./FolderTrustDialog.js', () => ({
|
||||
FolderTrustDialog: () => <Text>FolderTrustDialog</Text>,
|
||||
}));
|
||||
vi.mock('./ShellConfirmationDialog.js', () => ({
|
||||
ShellConfirmationDialog: () => <Text>ShellConfirmationDialog</Text>,
|
||||
}));
|
||||
vi.mock('./ConsentPrompt.js', () => ({
|
||||
ConsentPrompt: () => <Text>ConsentPrompt</Text>,
|
||||
}));
|
||||
vi.mock('./ThemeDialog.js', () => ({
|
||||
ThemeDialog: () => <Text>ThemeDialog</Text>,
|
||||
}));
|
||||
vi.mock('./SettingsDialog.js', () => ({
|
||||
SettingsDialog: () => <Text>SettingsDialog</Text>,
|
||||
}));
|
||||
vi.mock('../auth/AuthInProgress.js', () => ({
|
||||
AuthInProgress: () => <Text>AuthInProgress</Text>,
|
||||
}));
|
||||
vi.mock('../auth/AuthDialog.js', () => ({
|
||||
AuthDialog: () => <Text>AuthDialog</Text>,
|
||||
}));
|
||||
vi.mock('../auth/ApiAuthDialog.js', () => ({
|
||||
ApiAuthDialog: () => <Text>ApiAuthDialog</Text>,
|
||||
}));
|
||||
vi.mock('./EditorSettingsDialog.js', () => ({
|
||||
EditorSettingsDialog: () => <Text>EditorSettingsDialog</Text>,
|
||||
}));
|
||||
vi.mock('../privacy/PrivacyNotice.js', () => ({
|
||||
PrivacyNotice: () => <Text>PrivacyNotice</Text>,
|
||||
}));
|
||||
vi.mock('./ProQuotaDialog.js', () => ({
|
||||
ProQuotaDialog: () => <Text>ProQuotaDialog</Text>,
|
||||
}));
|
||||
vi.mock('./PermissionsModifyTrustDialog.js', () => ({
|
||||
PermissionsModifyTrustDialog: () => <Text>PermissionsModifyTrustDialog</Text>,
|
||||
}));
|
||||
vi.mock('./ModelDialog.js', () => ({
|
||||
ModelDialog: () => <Text>ModelDialog</Text>,
|
||||
}));
|
||||
vi.mock('./IdeTrustChangeDialog.js', () => ({
|
||||
IdeTrustChangeDialog: () => <Text>IdeTrustChangeDialog</Text>,
|
||||
}));
|
||||
|
||||
describe('DialogManager', () => {
|
||||
const defaultProps = {
|
||||
addItem: vi.fn(),
|
||||
terminalWidth: 100,
|
||||
};
|
||||
|
||||
const baseUiState = {
|
||||
constrainHeight: false,
|
||||
terminalHeight: 24,
|
||||
staticExtraHeight: 0,
|
||||
mainAreaWidth: 80,
|
||||
confirmUpdateExtensionRequests: [],
|
||||
showIdeRestartPrompt: false,
|
||||
proQuotaRequest: null,
|
||||
shouldShowIdePrompt: false,
|
||||
isFolderTrustDialogOpen: false,
|
||||
shellConfirmationRequest: null,
|
||||
loopDetectionConfirmationRequest: null,
|
||||
confirmationRequest: null,
|
||||
isThemeDialogOpen: false,
|
||||
isSettingsDialogOpen: false,
|
||||
isModelDialogOpen: false,
|
||||
isAuthenticating: false,
|
||||
isAwaitingApiKeyInput: false,
|
||||
isAuthDialogOpen: false,
|
||||
isEditorDialogOpen: false,
|
||||
showPrivacyNotice: false,
|
||||
isPermissionsDialogOpen: false,
|
||||
};
|
||||
|
||||
it('renders nothing by default', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<DialogManager {...defaultProps} />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ uiState: baseUiState as any },
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
const testCases: Array<[Partial<UIState>, string]> = [
|
||||
[
|
||||
{
|
||||
showIdeRestartPrompt: true,
|
||||
ideTrustRestartReason: 'update' as RestartReason,
|
||||
},
|
||||
'IdeTrustChangeDialog',
|
||||
],
|
||||
[
|
||||
{
|
||||
proQuotaRequest: {
|
||||
failedModel: 'a',
|
||||
fallbackModel: 'b',
|
||||
message: 'c',
|
||||
isTerminalQuotaError: false,
|
||||
resolve: vi.fn(),
|
||||
},
|
||||
},
|
||||
'ProQuotaDialog',
|
||||
],
|
||||
[
|
||||
{
|
||||
shouldShowIdePrompt: true,
|
||||
currentIDE: { name: 'vscode', version: '1.0' } as unknown as IdeInfo,
|
||||
},
|
||||
'IdeIntegrationNudge',
|
||||
],
|
||||
[{ isFolderTrustDialogOpen: true }, 'FolderTrustDialog'],
|
||||
[
|
||||
{
|
||||
shellConfirmationRequest: {
|
||||
commands: [],
|
||||
onConfirm: vi.fn(),
|
||||
} as unknown as ShellConfirmationRequest,
|
||||
},
|
||||
'ShellConfirmationDialog',
|
||||
],
|
||||
[
|
||||
{ loopDetectionConfirmationRequest: { onComplete: vi.fn() } },
|
||||
'LoopDetectionConfirmation',
|
||||
],
|
||||
[
|
||||
{ confirmationRequest: { prompt: 'foo', onConfirm: vi.fn() } },
|
||||
'ConsentPrompt',
|
||||
],
|
||||
[
|
||||
{
|
||||
confirmUpdateExtensionRequests: [{ prompt: 'foo', onConfirm: vi.fn() }],
|
||||
},
|
||||
'ConsentPrompt',
|
||||
],
|
||||
[{ isThemeDialogOpen: true }, 'ThemeDialog'],
|
||||
[{ isSettingsDialogOpen: true }, 'SettingsDialog'],
|
||||
[{ isModelDialogOpen: true }, 'ModelDialog'],
|
||||
[{ isAuthenticating: true }, 'AuthInProgress'],
|
||||
[{ isAwaitingApiKeyInput: true }, 'ApiAuthDialog'],
|
||||
[{ isAuthDialogOpen: true }, 'AuthDialog'],
|
||||
[{ isEditorDialogOpen: true }, 'EditorSettingsDialog'],
|
||||
[{ showPrivacyNotice: true }, 'PrivacyNotice'],
|
||||
[{ isPermissionsDialogOpen: true }, 'PermissionsModifyTrustDialog'],
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'renders %s when state is %o',
|
||||
(uiStateOverride, expectedComponent) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<DialogManager {...defaultProps} />,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
uiState: { ...baseUiState, ...uiStateOverride } as any,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toContain(expectedComponent);
|
||||
},
|
||||
);
|
||||
});
|
||||
167
packages/cli/src/ui/components/EditorSettingsDialog.test.tsx
Normal file
167
packages/cli/src/ui/components/EditorSettingsDialog.test.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
// Mock editorSettingsManager
|
||||
vi.mock('../editors/editorSettingsManager.js', () => ({
|
||||
editorSettingsManager: {
|
||||
getAvailableEditorDisplays: () => [
|
||||
{ name: 'VS Code', type: 'vscode', disabled: false },
|
||||
{ name: 'Vim', type: 'vim', disabled: false },
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
describe('EditorSettingsDialog', () => {
|
||||
const mockSettings = {
|
||||
forScope: (scope: string) => ({
|
||||
settings: {
|
||||
general: {
|
||||
preferredEditor: scope === SettingScope.User ? 'vscode' : undefined,
|
||||
},
|
||||
},
|
||||
}),
|
||||
merged: {
|
||||
general: {
|
||||
preferredEditor: 'vscode',
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderWithProvider = (ui: React.ReactNode) =>
|
||||
render(<KeypressProvider>{ui}</KeypressProvider>);
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { lastFrame } = renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={mockSettings}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls onSelect when an editor is selected', () => {
|
||||
const onSelect = vi.fn();
|
||||
const { lastFrame } = renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={onSelect}
|
||||
settings={mockSettings}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('VS Code');
|
||||
});
|
||||
|
||||
it('switches focus between editor and scope sections on Tab', async () => {
|
||||
const { lastFrame, stdin } = renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={mockSettings}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Initial focus on editor
|
||||
expect(lastFrame()).toContain('> Select Editor');
|
||||
expect(lastFrame()).not.toContain('> Apply To');
|
||||
|
||||
// Press Tab
|
||||
await act(async () => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
|
||||
// Focus should be on scope
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame() || '';
|
||||
if (!frame.includes('> Apply To')) {
|
||||
console.log(
|
||||
'Waiting for scope focus. Current frame:',
|
||||
JSON.stringify(frame),
|
||||
);
|
||||
}
|
||||
expect(frame).toContain('> Apply To');
|
||||
});
|
||||
expect(lastFrame()).toContain(' Select Editor');
|
||||
|
||||
// Press Tab again
|
||||
await act(async () => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
|
||||
// Focus should be back on editor
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('> Select Editor');
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onExit when Escape is pressed', async () => {
|
||||
const onExit = vi.fn();
|
||||
const { stdin } = renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={mockSettings}
|
||||
onExit={onExit}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B'); // Escape
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onExit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modified message when setting exists in other scope', () => {
|
||||
const settingsWithOtherScope = {
|
||||
forScope: (_scope: string) => ({
|
||||
settings: {
|
||||
general: {
|
||||
preferredEditor: 'vscode', // Both scopes have it set
|
||||
},
|
||||
},
|
||||
}),
|
||||
merged: {
|
||||
general: {
|
||||
preferredEditor: 'vscode',
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { lastFrame } = renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={settingsWithOtherScope}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const frame = lastFrame() || '';
|
||||
if (!frame.includes('(Also modified')) {
|
||||
console.log(
|
||||
'Modified message test failure. Frame:',
|
||||
JSON.stringify(frame),
|
||||
);
|
||||
}
|
||||
expect(frame).toContain('(Also modified');
|
||||
});
|
||||
});
|
||||
60
packages/cli/src/ui/components/ExitWarning.test.tsx
Normal file
60
packages/cli/src/ui/components/ExitWarning.test.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ExitWarning } from './ExitWarning.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
|
||||
describe('ExitWarning', () => {
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing by default', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
dialogsVisible: false,
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<ExitWarning />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders Ctrl+C warning when pressed once and dialogs visible', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
dialogsVisible: true,
|
||||
ctrlCPressedOnce: true,
|
||||
ctrlDPressedOnce: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<ExitWarning />);
|
||||
expect(lastFrame()).toContain('Press Ctrl+C again to exit');
|
||||
});
|
||||
|
||||
it('renders Ctrl+D warning when pressed once and dialogs visible', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
dialogsVisible: true,
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<ExitWarning />);
|
||||
expect(lastFrame()).toContain('Press Ctrl+D again to exit');
|
||||
});
|
||||
|
||||
it('renders nothing if dialogs are not visible', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
dialogsVisible: false,
|
||||
ctrlCPressedOnce: true,
|
||||
ctrlDPressedOnce: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<ExitWarning />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
||||
import { useIsScreenReaderEnabled } from 'ink';
|
||||
import { StreamingState } from '../types.js';
|
||||
import {
|
||||
SCREEN_READER_LOADING,
|
||||
SCREEN_READER_RESPONDING,
|
||||
} from '../textConstants.js';
|
||||
|
||||
vi.mock('../contexts/StreamingContext.js');
|
||||
vi.mock('ink', async () => {
|
||||
const actual = await vi.importActual('ink');
|
||||
return {
|
||||
...actual,
|
||||
useIsScreenReaderEnabled: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('GeminiRespondingSpinner', () => {
|
||||
const mockUseStreamingContext = vi.mocked(useStreamingContext);
|
||||
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders spinner when responding', () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
|
||||
const { lastFrame } = render(<GeminiRespondingSpinner />);
|
||||
// Spinner output varies, but it shouldn't be empty
|
||||
expect(lastFrame()).not.toBe('');
|
||||
});
|
||||
|
||||
it('renders screen reader text when responding and screen reader enabled', () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
const { lastFrame } = render(<GeminiRespondingSpinner />);
|
||||
expect(lastFrame()).toContain(SCREEN_READER_RESPONDING);
|
||||
});
|
||||
|
||||
it('renders nothing when not responding and no non-responding display', () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
const { lastFrame } = render(<GeminiRespondingSpinner />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders non-responding display when provided', () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
const { lastFrame } = render(
|
||||
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
|
||||
);
|
||||
expect(lastFrame()).toContain('Waiting...');
|
||||
});
|
||||
|
||||
it('renders screen reader loading text when non-responding display provided and screen reader enabled', () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
|
||||
);
|
||||
expect(lastFrame()).toContain(SCREEN_READER_LOADING);
|
||||
});
|
||||
});
|
||||
103
packages/cli/src/ui/components/MainContent.test.tsx
Normal file
103
packages/cli/src/ui/components/MainContent.test.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { MainContent } from './MainContent.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../contexts/AppContext.js', () => ({
|
||||
useAppContext: () => ({
|
||||
version: '1.0.0',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: () => ({
|
||||
history: [
|
||||
{ id: 1, role: 'user', content: 'Hello' },
|
||||
{ id: 2, role: 'model', content: 'Hi there' },
|
||||
],
|
||||
pendingHistoryItems: [],
|
||||
mainAreaWidth: 80,
|
||||
staticAreaMaxItemHeight: 20,
|
||||
availableTerminalHeight: 24,
|
||||
slashCommands: [],
|
||||
constrainHeight: false,
|
||||
isEditorDialogOpen: false,
|
||||
activePtyId: undefined,
|
||||
embeddedShellFocused: false,
|
||||
historyRemountKey: 0,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./HistoryItemDisplay.js', () => ({
|
||||
HistoryItemDisplay: ({ item }: { item: { content: string } }) => (
|
||||
<Box>
|
||||
<Text>HistoryItem: {item.content}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./AppHeader.js', () => ({
|
||||
AppHeader: () => <Text>AppHeader</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./ShowMoreLines.js', () => ({
|
||||
ShowMoreLines: () => <Text>ShowMoreLines</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./shared/ScrollableList.js', () => ({
|
||||
ScrollableList: ({
|
||||
data,
|
||||
renderItem,
|
||||
}: {
|
||||
data: unknown[];
|
||||
renderItem: (props: { item: unknown }) => React.JSX.Element;
|
||||
}) => (
|
||||
<Box flexDirection="column">
|
||||
<Text>ScrollableList</Text>
|
||||
{data.map((item: unknown, index: number) => (
|
||||
<Box key={index}>{renderItem({ item })}</Box>
|
||||
))}
|
||||
</Box>
|
||||
),
|
||||
SCROLL_TO_ITEM_END: 0,
|
||||
}));
|
||||
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
|
||||
describe('MainContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders in normal buffer mode', () => {
|
||||
const { lastFrame } = render(<MainContent />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('AppHeader');
|
||||
expect(output).toContain('HistoryItem: Hello');
|
||||
expect(output).toContain('HistoryItem: Hi there');
|
||||
});
|
||||
|
||||
it('renders in alternate buffer mode', () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame } = render(<MainContent />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('ScrollableList');
|
||||
expect(output).toContain('AppHeader');
|
||||
expect(output).toContain('HistoryItem: Hello');
|
||||
expect(output).toContain('HistoryItem: Hi there');
|
||||
});
|
||||
});
|
||||
55
packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx
Normal file
55
packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import process from 'node:process';
|
||||
import { act } from 'react';
|
||||
|
||||
describe('MemoryUsageDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
// Mock process.memoryUsage
|
||||
vi.spyOn(process, 'memoryUsage').mockReturnValue({
|
||||
rss: 1024 * 1024 * 50, // 50MB
|
||||
heapTotal: 0,
|
||||
heapUsed: 0,
|
||||
external: 0,
|
||||
arrayBuffers: 0,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders memory usage', () => {
|
||||
const { lastFrame } = render(<MemoryUsageDisplay />);
|
||||
expect(lastFrame()).toContain('50.0 MB');
|
||||
});
|
||||
|
||||
it('updates memory usage over time', async () => {
|
||||
const { lastFrame } = render(<MemoryUsageDisplay />);
|
||||
expect(lastFrame()).toContain('50.0 MB');
|
||||
|
||||
vi.mocked(process.memoryUsage).mockReturnValue({
|
||||
rss: 1024 * 1024 * 100, // 100MB
|
||||
heapTotal: 0,
|
||||
heapUsed: 0,
|
||||
external: 0,
|
||||
arrayBuffers: 0,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(2000);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('100.0 MB');
|
||||
});
|
||||
});
|
||||
183
packages/cli/src/ui/components/Notifications.test.tsx
Normal file
183
packages/cli/src/ui/components/Notifications.test.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Notifications } from './Notifications.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAppContext, type AppState } from '../contexts/AppContext.js';
|
||||
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
import { useIsScreenReaderEnabled } from 'ink';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { act } from 'react';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../contexts/AppContext.js');
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
vi.mock('ink', async () => {
|
||||
const actual = await vi.importActual('ink');
|
||||
return {
|
||||
...actual,
|
||||
useIsScreenReaderEnabled: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('node:fs/promises', async () => {
|
||||
const actual = await vi.importActual('node:fs/promises');
|
||||
return {
|
||||
...actual,
|
||||
access: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
vi.mock('node:os', () => ({
|
||||
default: {
|
||||
homedir: () => '/mock/home',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('node:path', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:path')>('node:path');
|
||||
return {
|
||||
...actual,
|
||||
default: actual.posix,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
GEMINI_DIR: '.gemini',
|
||||
Storage: {
|
||||
getGlobalTempDir: () => '/mock/temp',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
DEFAULT_MODEL_CONFIGS: {},
|
||||
LoadedSettings: class {
|
||||
constructor() {
|
||||
// this.merged = {};
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Notifications', () => {
|
||||
const mockUseAppContext = vi.mocked(useAppContext);
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
|
||||
const mockFsAccess = vi.mocked(fs.access);
|
||||
const mockFsWriteFile = vi.mocked(fs.writeFile);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseAppContext.mockReturnValue({
|
||||
startupWarnings: [],
|
||||
version: '1.0.0',
|
||||
} as AppState);
|
||||
mockUseUIState.mockReturnValue({
|
||||
initError: null,
|
||||
streamingState: 'idle',
|
||||
updateInfo: null,
|
||||
} as unknown as UIState);
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders nothing when no notifications', () => {
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it.each([[['Warning 1']], [['Warning 1', 'Warning 2']]])(
|
||||
'renders startup warnings: %s',
|
||||
(warnings) => {
|
||||
mockUseAppContext.mockReturnValue({
|
||||
startupWarnings: warnings,
|
||||
version: '1.0.0',
|
||||
} as AppState);
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
const output = lastFrame();
|
||||
warnings.forEach((warning) => {
|
||||
expect(output).toContain(warning);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('renders init error', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
initError: 'Something went wrong',
|
||||
streamingState: 'idle',
|
||||
updateInfo: null,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render init error when streaming', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
initError: 'Something went wrong',
|
||||
streamingState: 'responding',
|
||||
updateInfo: null,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders update notification', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
initError: null,
|
||||
streamingState: 'idle',
|
||||
updateInfo: { message: 'Update available' },
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders screen reader nudge when enabled and not seen', async () => {
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
|
||||
let rejectAccess: (err: Error) => void;
|
||||
mockFsAccess.mockImplementation(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectAccess = reject;
|
||||
}),
|
||||
);
|
||||
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
|
||||
// Trigger rejection inside act
|
||||
await act(async () => {
|
||||
rejectAccess(new Error('File not found'));
|
||||
});
|
||||
|
||||
// Wait for effect to propagate
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFsWriteFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render screen reader nudge when already seen', async () => {
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
|
||||
let resolveAccess: (val: undefined) => void;
|
||||
mockFsAccess.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveAccess = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const { lastFrame } = render(<Notifications />);
|
||||
|
||||
// Trigger resolution inside act
|
||||
await act(async () => {
|
||||
resolveAccess(undefined);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
expect(mockFsWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export const Notifications = () => {
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const checkScreenReaderNudge = async () => {
|
||||
const checkScreenReader = async () => {
|
||||
try {
|
||||
await fs.access(screenReaderNudgeFilePath);
|
||||
setHasSeenScreenReaderNudge(true);
|
||||
@@ -47,8 +47,11 @@ export const Notifications = () => {
|
||||
setHasSeenScreenReaderNudge(false);
|
||||
}
|
||||
};
|
||||
checkScreenReaderNudge();
|
||||
}, []);
|
||||
|
||||
if (isScreenReaderEnabled) {
|
||||
checkScreenReader();
|
||||
}
|
||||
}, [isScreenReaderEnabled]);
|
||||
|
||||
const showScreenReaderNudge =
|
||||
isScreenReaderEnabled && hasSeenScreenReaderNudge === false;
|
||||
|
||||
54
packages/cli/src/ui/components/QuittingDisplay.test.tsx
Normal file
54
packages/cli/src/ui/components/QuittingDisplay.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { QuittingDisplay } from './QuittingDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
vi.mock('./HistoryItemDisplay.js', async () => {
|
||||
const { Text } = await vi.importActual('ink');
|
||||
return {
|
||||
HistoryItemDisplay: ({ item }: { item: { content: string } }) =>
|
||||
React.createElement(Text as unknown as React.FC, null, item.content),
|
||||
};
|
||||
});
|
||||
|
||||
describe('QuittingDisplay', () => {
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
const mockUseTerminalSize = vi.mocked(useTerminalSize);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseTerminalSize.mockReturnValue({ rows: 20, columns: 80 });
|
||||
});
|
||||
|
||||
it('renders nothing when no quitting messages', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
quittingMessages: null,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<QuittingDisplay />);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders quitting messages', () => {
|
||||
const mockMessages = [
|
||||
{ id: '1', type: 'user', content: 'Goodbye' },
|
||||
{ id: '2', type: 'model', content: 'See you later' },
|
||||
];
|
||||
mockUseUIState.mockReturnValue({
|
||||
quittingMessages: mockMessages,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame } = render(<QuittingDisplay />);
|
||||
expect(lastFrame()).toContain('Goodbye');
|
||||
expect(lastFrame()).toContain('See you later');
|
||||
});
|
||||
});
|
||||
37
packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx
Normal file
37
packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
|
||||
describe('RawMarkdownIndicator', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correct key binding for darwin', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const { lastFrame } = render(<RawMarkdownIndicator />);
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('option+m to toggle');
|
||||
});
|
||||
|
||||
it('renders correct key binding for other platforms', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const { lastFrame } = render(<RawMarkdownIndicator />);
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('alt+m to toggle');
|
||||
});
|
||||
});
|
||||
@@ -157,8 +157,7 @@ describe('SessionBrowser component', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('No auto-saved conversations found.');
|
||||
expect(lastFrame()).toContain('Press q to exit');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a list of sessions and marks current session as disabled', () => {
|
||||
@@ -193,11 +192,7 @@ describe('SessionBrowser component', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Chat Sessions (2 total');
|
||||
expect(output).toContain('First conversation about cats');
|
||||
expect(output).toContain('Second conversation about dogs');
|
||||
expect(output).toContain('(current)');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('enters search mode, filters sessions, and renders match snippets', async () => {
|
||||
@@ -214,6 +209,7 @@ describe('SessionBrowser component', () => {
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
lastUpdated: '2025-01-01T12:00:00Z',
|
||||
});
|
||||
|
||||
const otherSession = createSession({
|
||||
@@ -229,6 +225,7 @@ describe('SessionBrowser component', () => {
|
||||
},
|
||||
],
|
||||
index: 1,
|
||||
lastUpdated: '2025-01-01T10:00:00Z',
|
||||
});
|
||||
|
||||
const config = createMockConfig();
|
||||
@@ -259,15 +256,9 @@ describe('SessionBrowser component', () => {
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Chat Sessions (1 total, filtered');
|
||||
expect(output).toContain('Query is here');
|
||||
expect(output).not.toContain('Nothing interesting here.');
|
||||
|
||||
expect(output).toContain('You:');
|
||||
expect(output).toContain('query');
|
||||
expect(output).toContain('(+1 more)');
|
||||
expect(lastFrame()).toContain('Chat Sessions (1 total, filtered');
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles keyboard navigation and resumes the selected session', () => {
|
||||
@@ -276,12 +267,14 @@ describe('SessionBrowser component', () => {
|
||||
file: 'one',
|
||||
displayName: 'First session',
|
||||
index: 0,
|
||||
lastUpdated: '2025-01-02T12:00:00Z',
|
||||
});
|
||||
const session2 = createSession({
|
||||
id: 'two',
|
||||
file: 'two',
|
||||
displayName: 'Second session',
|
||||
index: 1,
|
||||
lastUpdated: '2025-01-01T12:00:00Z',
|
||||
});
|
||||
|
||||
const config = createMockConfig();
|
||||
@@ -317,6 +310,7 @@ describe('SessionBrowser component', () => {
|
||||
displayName: 'Current session',
|
||||
isCurrentSession: true,
|
||||
index: 0,
|
||||
lastUpdated: '2025-01-02T12:00:00Z',
|
||||
});
|
||||
const otherSession = createSession({
|
||||
id: 'other',
|
||||
@@ -324,6 +318,7 @@ describe('SessionBrowser component', () => {
|
||||
displayName: 'Other session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
lastUpdated: '2025-01-01T12:00:00Z',
|
||||
});
|
||||
|
||||
const config = createMockConfig();
|
||||
@@ -364,8 +359,6 @@ describe('SessionBrowser component', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Error: storage failure');
|
||||
expect(output).toContain('Press q to exit');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
105
packages/cli/src/ui/components/ShellInputPrompt.test.tsx
Normal file
105
packages/cli/src/ui/components/ShellInputPrompt.test.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ShellInputPrompt } from './ShellInputPrompt.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock useKeypress
|
||||
const mockUseKeypress = vi.fn();
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: (handler: (input: unknown) => void, options?: unknown) =>
|
||||
mockUseKeypress(handler, options),
|
||||
}));
|
||||
|
||||
// Mock ShellExecutionService
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
ShellExecutionService: {
|
||||
writeToPty: vi.fn(),
|
||||
scrollPty: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('ShellInputPrompt', () => {
|
||||
const mockWriteToPty = vi.mocked(ShellExecutionService.writeToPty);
|
||||
const mockScrollPty = vi.mocked(ShellExecutionService.scrollPty);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing', () => {
|
||||
const { lastFrame } = render(
|
||||
<ShellInputPrompt activeShellPtyId={1} focus={true} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a', 'a'],
|
||||
['b', 'b'],
|
||||
])('handles keypress input: %s', (name, sequence) => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||
|
||||
// Get the registered handler
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
// Simulate keypress
|
||||
handler({ name, sequence, ctrl: false, shift: false, meta: false });
|
||||
|
||||
expect(mockWriteToPty).toHaveBeenCalledWith(1, sequence);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['up', -1],
|
||||
['down', 1],
|
||||
])('handles scroll %s (Ctrl+Shift+%s)', (key, direction) => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
handler({ name: key, ctrl: true, shift: true, meta: false });
|
||||
|
||||
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
|
||||
});
|
||||
|
||||
it('does not handle input when not focused', () => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={false} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
handler({
|
||||
name: 'a',
|
||||
sequence: 'a',
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
meta: false,
|
||||
});
|
||||
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not handle input when no active shell', () => {
|
||||
render(<ShellInputPrompt activeShellPtyId={null} focus={true} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
handler({
|
||||
name: 'a',
|
||||
sequence: 'a',
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
meta: false,
|
||||
});
|
||||
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
17
packages/cli/src/ui/components/ShellModeIndicator.test.tsx
Normal file
17
packages/cli/src/ui/components/ShellModeIndicator.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ShellModeIndicator', () => {
|
||||
it('renders correctly', () => {
|
||||
const { lastFrame } = render(<ShellModeIndicator />);
|
||||
expect(lastFrame()).toContain('shell mode enabled');
|
||||
expect(lastFrame()).toContain('esc to disable');
|
||||
});
|
||||
});
|
||||
54
packages/cli/src/ui/components/ShowMoreLines.test.tsx
Normal file
54
packages/cli/src/ui/components/ShowMoreLines.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useOverflowState } from '../contexts/OverflowContext.js';
|
||||
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
|
||||
vi.mock('../contexts/OverflowContext.js');
|
||||
vi.mock('../contexts/StreamingContext.js');
|
||||
|
||||
describe('ShowMoreLines', () => {
|
||||
const mockUseOverflowState = vi.mocked(useOverflowState);
|
||||
const mockUseStreamingContext = vi.mocked(useStreamingContext);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[new Set(), StreamingState.Idle, true], // No overflow
|
||||
[new Set(['1']), StreamingState.Idle, false], // Not constraining height
|
||||
[new Set(['1']), StreamingState.Responding, true], // Streaming
|
||||
])(
|
||||
'renders nothing when: overflow=%s, streaming=%s, constrain=%s',
|
||||
(overflowingIds, streamingState, constrainHeight) => {
|
||||
mockUseOverflowState.mockReturnValue({ overflowingIds } as NonNullable<
|
||||
ReturnType<typeof useOverflowState>
|
||||
>);
|
||||
mockUseStreamingContext.mockReturnValue(streamingState);
|
||||
const { lastFrame } = render(
|
||||
<ShowMoreLines constrainHeight={constrainHeight} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([[StreamingState.Idle], [StreamingState.WaitingForConfirmation]])(
|
||||
'renders message when overflowing and state is %s',
|
||||
(streamingState) => {
|
||||
mockUseOverflowState.mockReturnValue({
|
||||
overflowingIds: new Set(['1']),
|
||||
} as NonNullable<ReturnType<typeof useOverflowState>>);
|
||||
mockUseStreamingContext.mockReturnValue(streamingState);
|
||||
const { lastFrame } = render(<ShowMoreLines constrainHeight={true} />);
|
||||
expect(lastFrame()).toContain('Press ctrl-s to show more lines');
|
||||
},
|
||||
);
|
||||
});
|
||||
124
packages/cli/src/ui/components/SuggestionsDisplay.test.tsx
Normal file
124
packages/cli/src/ui/components/SuggestionsDisplay.test.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
|
||||
describe('SuggestionsDisplay', () => {
|
||||
const mockSuggestions = [
|
||||
{ label: 'Command 1', value: 'command1', description: 'Description 1' },
|
||||
{ label: 'Command 2', value: 'command2', description: 'Description 2' },
|
||||
{ label: 'Command 3', value: 'command3', description: 'Description 3' },
|
||||
];
|
||||
|
||||
it('renders loading state', () => {
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={[]}
|
||||
activeIndex={0}
|
||||
isLoading={true}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders nothing when empty and not loading', () => {
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={[]}
|
||||
activeIndex={0}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders suggestions list', () => {
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={mockSuggestions}
|
||||
activeIndex={0}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('highlights active item', () => {
|
||||
// This test relies on visual inspection or implementation details (colors)
|
||||
// For now, we just ensure it renders without error and contains the item
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={mockSuggestions}
|
||||
activeIndex={1}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles scrolling', () => {
|
||||
const manySuggestions = Array.from({ length: 20 }, (_, i) => ({
|
||||
label: `Cmd ${i}`,
|
||||
value: `Cmd ${i}`,
|
||||
description: `Description ${i}`,
|
||||
}));
|
||||
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={manySuggestions}
|
||||
activeIndex={10}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={5}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders MCP tag for MCP prompts', () => {
|
||||
const mcpSuggestions = [
|
||||
{
|
||||
label: 'MCP Tool',
|
||||
value: 'mcp-tool',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<SuggestionsDisplay
|
||||
suggestions={mcpSuggestions}
|
||||
activeIndex={0}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
32
packages/cli/src/ui/components/ThemedGradient.test.tsx
Normal file
32
packages/cli/src/ui/components/ThemedGradient.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock theme to control gradient
|
||||
vi.mock('../semantic-colors.js', () => ({
|
||||
theme: {
|
||||
ui: {
|
||||
gradient: ['red', 'blue'],
|
||||
},
|
||||
text: {
|
||||
accent: 'cyan',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ThemedGradient', () => {
|
||||
it('renders children', () => {
|
||||
const { lastFrame } = render(<ThemedGradient>Hello</ThemedGradient>);
|
||||
expect(lastFrame()).toContain('Hello');
|
||||
});
|
||||
|
||||
// Note: Testing actual gradient application is hard with ink-testing-library
|
||||
// as it often renders as plain text or ANSI codes.
|
||||
// We mainly ensure it doesn't crash and renders content.
|
||||
});
|
||||
25
packages/cli/src/ui/components/Tips.test.tsx
Normal file
25
packages/cli/src/ui/components/Tips.test.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Tips } from './Tips.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Tips', () => {
|
||||
it.each([
|
||||
[0, '3. Create GEMINI.md files'],
|
||||
[5, '3. /help for more information'],
|
||||
])('renders correct tips when file count is %i', (count, expectedText) => {
|
||||
const config = {
|
||||
getGeminiMdFileCount: vi.fn().mockReturnValue(count),
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame } = render(<Tips config={config} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(expectedText);
|
||||
});
|
||||
});
|
||||
18
packages/cli/src/ui/components/UpdateNotification.test.tsx
Normal file
18
packages/cli/src/ui/components/UpdateNotification.test.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { UpdateNotification } from './UpdateNotification.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('UpdateNotification', () => {
|
||||
it('renders message', () => {
|
||||
const { lastFrame } = render(
|
||||
<UpdateNotification message="Update available!" />,
|
||||
);
|
||||
expect(lastFrame()).toContain('Update available!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Banner > handles newlines in text 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Line 1 │
|
||||
│ Line 2 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`Banner > renders in info mode 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Info Message │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`Banner > renders in warning mode 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Warning Message │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ConfigInitDisplay > handles empty clients map 1`] = `
|
||||
"
|
||||
Spinner Initializing..."
|
||||
`;
|
||||
|
||||
exports[`ConfigInitDisplay > renders initial state 1`] = `
|
||||
"
|
||||
Spinner Initializing..."
|
||||
`;
|
||||
|
||||
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
|
||||
"
|
||||
Spinner Connecting to MCP servers... (1/2)"
|
||||
`;
|
||||
@@ -0,0 +1,18 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`EditorSettingsDialog > renders correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Select Editor Editor Preference │
|
||||
│ ● 1. VS Code │
|
||||
│ 2. Vim These editors are currently supported. Please note │
|
||||
│ that some editors cannot be used in sandbox mode. │
|
||||
│ Apply To │
|
||||
│ ● 1. User Settings Your preferred editor is: None. │
|
||||
│ 2. Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change │
|
||||
│ focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -0,0 +1,22 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Notifications > renders init error 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Initialization Error: Something went wrong Please check API key and configuration. │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Notifications > renders screen reader nudge when enabled and not seen 1`] = `
|
||||
"You are currently in screen reader-friendly view. To switch out, open
|
||||
/mock/home/.gemini/settings.json and remove the entry for "screenReader". This will disappear on
|
||||
next run."
|
||||
`;
|
||||
|
||||
exports[`Notifications > renders update notification 1`] = `
|
||||
"
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Update available │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SessionBrowser component > enters search mode, filters sessions, and renders match snippets 1`] = `
|
||||
" Chat Sessions (1 total, filtered) sorted by date desc
|
||||
|
||||
Search: query (Esc to cancel)
|
||||
|
||||
Index │ Msgs │ Age │ Match
|
||||
❯ #1 │ 1 │ 10mo │ You: Query is here a… (+1 more)
|
||||
▼"
|
||||
`;
|
||||
|
||||
exports[`SessionBrowser component > renders a list of sessions and marks current session as disabled 1`] = `
|
||||
" Chat Sessions (2 total) sorted by date desc
|
||||
Navigate: ↑/↓ Resume: Enter Search: / Delete: x Quit: q
|
||||
Sort: s Reverse: r First/Last: g/G
|
||||
|
||||
Index │ Msgs │ Age │ Name
|
||||
❯ #1 │ 5 │ 10mo │ Second conversation about dogs (current)
|
||||
#2 │ 2 │ 10mo │ First conversation about cats
|
||||
▼"
|
||||
`;
|
||||
|
||||
exports[`SessionBrowser component > shows an error state when loading sessions fails 1`] = `
|
||||
" Error: storage failure
|
||||
Press q to exit"
|
||||
`;
|
||||
|
||||
exports[`SessionBrowser component > shows empty state when no sessions exist 1`] = `
|
||||
" No auto-saved conversations found.
|
||||
Press q to exit"
|
||||
`;
|
||||
@@ -0,0 +1,31 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SuggestionsDisplay > handles scrolling 1`] = `
|
||||
" ▲
|
||||
Cmd 5 Description 5
|
||||
Cmd 6 Description 6
|
||||
Cmd 7 Description 7
|
||||
Cmd 8 Description 8
|
||||
Cmd 9 Description 9
|
||||
Cmd 10 Description 10
|
||||
Cmd 11 Description 11
|
||||
Cmd 12 Description 12
|
||||
▼
|
||||
(11/20)"
|
||||
`;
|
||||
|
||||
exports[`SuggestionsDisplay > highlights active item 1`] = `
|
||||
" command1 Description 1
|
||||
command2 Description 2
|
||||
command3 Description 3"
|
||||
`;
|
||||
|
||||
exports[`SuggestionsDisplay > renders MCP tag for MCP prompts 1`] = `" mcp-tool [MCP]"`;
|
||||
|
||||
exports[`SuggestionsDisplay > renders loading state 1`] = `" Loading suggestions..."`;
|
||||
|
||||
exports[`SuggestionsDisplay > renders suggestions list 1`] = `
|
||||
" command1 Description 1
|
||||
command2 Description 2
|
||||
command3 Description 3"
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { ErrorMessage } from './ErrorMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ErrorMessage', () => {
|
||||
it('renders with the correct prefix and text', () => {
|
||||
const { lastFrame } = render(<ErrorMessage text="Something went wrong" />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders multiline error messages', () => {
|
||||
const message = 'Error line 1\nError line 2';
|
||||
const { lastFrame } = render(<ErrorMessage text={message} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
35
packages/cli/src/ui/components/messages/InfoMessage.test.tsx
Normal file
35
packages/cli/src/ui/components/messages/InfoMessage.test.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { InfoMessage } from './InfoMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('InfoMessage', () => {
|
||||
it('renders with the correct default prefix and text', () => {
|
||||
const { lastFrame } = render(<InfoMessage text="Just so you know" />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with a custom icon', () => {
|
||||
const { lastFrame } = render(
|
||||
<InfoMessage text="Custom icon test" icon="★" />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders multiline info messages', () => {
|
||||
const message = 'Info line 1\nInfo line 2';
|
||||
const { lastFrame } = render(<InfoMessage text={message} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain('URLs to fetch:');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should display urls if prompt and url are different', () => {
|
||||
@@ -60,10 +60,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('URLs to fetch:');
|
||||
expect(lastFrame()).toContain(
|
||||
'- https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('with folder trust', () => {
|
||||
@@ -124,7 +121,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
details: mcpConfirmationDetails,
|
||||
alwaysAllowText: 'always allow',
|
||||
},
|
||||
])('$description', ({ details, alwaysAllowText }) => {
|
||||
])('$description', ({ details }) => {
|
||||
it('should show "allow always" when folder is trusted', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
@@ -140,7 +137,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain(alwaysAllowText);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should NOT show "allow always" when folder is untrusted', () => {
|
||||
@@ -158,7 +155,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain(alwaysAllowText);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,10 +107,7 @@ describe('<ToolMessage />', () => {
|
||||
StreamingState.Idle,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('✓'); // Success indicator
|
||||
expect(output).toContain('test-tool');
|
||||
expect(output).toContain('A tool for testing');
|
||||
expect(output).toContain('MockMarkdown:Test result');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('ToolStatusIndicator rendering', () => {
|
||||
@@ -119,7 +116,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Success} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('✓');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows o for Pending status', () => {
|
||||
@@ -127,7 +124,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Pending} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('o');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows ? for Confirming status', () => {
|
||||
@@ -135,7 +132,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Confirming} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('?');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows - for Canceled status', () => {
|
||||
@@ -143,7 +140,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Canceled} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('-');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows x for Error status', () => {
|
||||
@@ -151,7 +148,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Error} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('x');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows paused spinner for Executing status when streamingState is Idle', () => {
|
||||
@@ -159,9 +156,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('⊷');
|
||||
expect(lastFrame()).not.toContain('MockRespondingSpinner');
|
||||
expect(lastFrame()).not.toContain('✓');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => {
|
||||
@@ -169,9 +164,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
|
||||
StreamingState.WaitingForConfirmation,
|
||||
);
|
||||
expect(lastFrame()).toContain('⊷');
|
||||
expect(lastFrame()).not.toContain('MockRespondingSpinner');
|
||||
expect(lastFrame()).not.toContain('✓');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => {
|
||||
@@ -179,8 +172,7 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
|
||||
StreamingState.Responding, // Simulate app still responding
|
||||
);
|
||||
expect(lastFrame()).toContain('MockRespondingSpinner');
|
||||
expect(lastFrame()).not.toContain('✓');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,7 +188,7 @@ describe('<ToolMessage />', () => {
|
||||
StreamingState.Idle,
|
||||
);
|
||||
// Check that the output contains the MockDiff content as part of the whole message
|
||||
expect(lastFrame()).toMatch(/MockDiff:--- a\/file\.txt/);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders emphasis correctly', () => {
|
||||
@@ -205,7 +197,7 @@ describe('<ToolMessage />', () => {
|
||||
StreamingState.Idle,
|
||||
);
|
||||
// Check for trailing indicator or specific color if applicable (Colors are not easily testable here)
|
||||
expect(highEmphasisFrame()).toContain('←'); // Trailing indicator for high emphasis
|
||||
expect(highEmphasisFrame()).toMatchSnapshot();
|
||||
|
||||
const { lastFrame: lowEmphasisFrame } = renderWithContext(
|
||||
<ToolMessage {...baseProps} emphasis="low" />,
|
||||
@@ -214,7 +206,7 @@ describe('<ToolMessage />', () => {
|
||||
// For low emphasis, the name and description might be dimmed (check for dimColor if possible)
|
||||
// This is harder to assert directly in text output without color checks.
|
||||
// We can at least ensure it doesn't have the high emphasis indicator.
|
||||
expect(lowEmphasisFrame()).not.toContain('←');
|
||||
expect(lowEmphasisFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders AnsiOutputText for AnsiOutput results', () => {
|
||||
@@ -236,6 +228,6 @@ describe('<ToolMessage />', () => {
|
||||
<ToolMessage {...baseProps} resultDisplay={ansiResult} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
expect(lastFrame()).toContain('MockAnsiOutput:hello');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock child components to simplify testing
|
||||
vi.mock('./DiffRenderer.js', () => ({
|
||||
DiffRenderer: ({
|
||||
diffContent,
|
||||
filename,
|
||||
}: {
|
||||
diffContent: string;
|
||||
filename: string;
|
||||
}) => (
|
||||
<Box>
|
||||
<Text>
|
||||
DiffRenderer: {filename} - {diffContent}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
||||
MarkdownDisplay: ({ text }: { text: string }) => (
|
||||
<Box>
|
||||
<Text>MarkdownDisplay: {text}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../AnsiOutput.js', () => ({
|
||||
AnsiOutputText: ({ data }: { data: unknown }) => (
|
||||
<Box>
|
||||
<Text>AnsiOutputText: {JSON.stringify(data)}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/MaxSizedBox.js', () => ({
|
||||
MaxSizedBox: ({ children }: { children: React.ReactNode }) => (
|
||||
<Box>
|
||||
<Text>MaxSizedBox:</Text>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock UIStateContext
|
||||
const mockUseUIState = vi.fn();
|
||||
vi.mock('../../contexts/UIStateContext.js', () => ({
|
||||
useUIState: () => mockUseUIState(),
|
||||
}));
|
||||
|
||||
// Mock useAlternateBuffer
|
||||
const mockUseAlternateBuffer = vi.fn();
|
||||
vi.mock('../../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: () => mockUseAlternateBuffer(),
|
||||
}));
|
||||
|
||||
describe('ToolResultDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseUIState.mockReturnValue({ renderMarkdown: true });
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders string result as markdown by default', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay resultDisplay="Some result" terminalWidth={80} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders string result as plain text when renderOutputAsMarkdown is false', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={false}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates very long string results', { timeout: 20000 }, () => {
|
||||
const longString = 'a'.repeat(1000005);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={longString}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders file diff result', () => {
|
||||
const diffResult = {
|
||||
fileDiff: 'diff content',
|
||||
fileName: 'test.ts',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={diffResult}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders ANSI output result', () => {
|
||||
const ansiResult = {
|
||||
text: 'ansi content',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult as unknown as AnsiOutput}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders nothing for todos result', () => {
|
||||
const todoResult = {
|
||||
todos: [],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={todoResult}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('falls back to plain text if availableHeight is set and not in alternate buffer', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// Should force renderOutputAsMarkdown to false
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('keeps markdown if in alternate buffer even with availableHeight', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ const MIN_LINES_SHOWN = 2; // show at least this many lines
|
||||
|
||||
// Large threshold to ensure we don't cause performance issues for very large
|
||||
// outputs that will get truncated further MaxSizedBox anyway.
|
||||
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
|
||||
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 20000;
|
||||
|
||||
export interface ToolResultDisplayProps {
|
||||
resultDisplay: string | object | undefined;
|
||||
|
||||
40
packages/cli/src/ui/components/messages/UserMessage.test.tsx
Normal file
40
packages/cli/src/ui/components/messages/UserMessage.test.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { UserMessage } from './UserMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the commandUtils to control isSlashCommand behavior
|
||||
vi.mock('../../utils/commandUtils.js', () => ({
|
||||
isSlashCommand: vi.fn((text: string) => text.startsWith('/')),
|
||||
}));
|
||||
|
||||
describe('UserMessage', () => {
|
||||
it('renders normal user message with correct prefix', () => {
|
||||
const { lastFrame } = render(
|
||||
<UserMessage text="Hello Gemini" width={80} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders slash command message', () => {
|
||||
const { lastFrame } = render(<UserMessage text="/help" width={80} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders multiline user message', () => {
|
||||
const message = 'Line 1\nLine 2';
|
||||
const { lastFrame } = render(<UserMessage text={message} width={80} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { WarningMessage } from './WarningMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('WarningMessage', () => {
|
||||
it('renders with the correct prefix and text', () => {
|
||||
const { lastFrame } = render(<WarningMessage text="Watch out!" />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders multiline warning messages', () => {
|
||||
const message = 'Warning line 1\nWarning line 2';
|
||||
const { lastFrame } = render(<WarningMessage text={message} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ErrorMessage > renders multiline error messages 1`] = `
|
||||
"✕ Error line 1
|
||||
Error line 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ErrorMessage > renders with the correct prefix and text 1`] = `
|
||||
"✕ Something went wrong
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InfoMessage > renders multiline info messages 1`] = `
|
||||
"
|
||||
ℹ Info line 1
|
||||
Info line 2"
|
||||
`;
|
||||
|
||||
exports[`InfoMessage > renders with a custom icon 1`] = `
|
||||
"
|
||||
★Custom icon test"
|
||||
`;
|
||||
|
||||
exports[`InfoMessage > renders with the correct default prefix and text 1`] = `
|
||||
"
|
||||
ℹ Just so you know"
|
||||
`;
|
||||
@@ -0,0 +1,123 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolConfirmationMessage > should display urls if prompt and url are different 1`] = `
|
||||
"fetch https://github.com/google/gemini-react/blob/main/README.md
|
||||
|
||||
URLs to fetch:
|
||||
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
|
||||
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, allow always
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > should not display urls if prompt and url are the same 1`] = `
|
||||
"https://example.com
|
||||
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, allow always
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
|
||||
"╭──────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────╯
|
||||
|
||||
Apply this change?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Modify with external editor
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should show "allow always" when folder is trusted 1`] = `
|
||||
"╭──────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────╯
|
||||
|
||||
Apply this change?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, allow always
|
||||
3. Modify with external editor
|
||||
4. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
|
||||
"echo "hello"
|
||||
|
||||
Allow execution of: 'echo'?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should show "allow always" when folder is trusted 1`] = `
|
||||
"echo "hello"
|
||||
|
||||
Allow execution of: 'echo'?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, allow always ...
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
|
||||
"https://example.com
|
||||
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations' > should show "allow always" when folder is trusted 1`] = `
|
||||
"https://example.com
|
||||
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, allow always
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
|
||||
"MCP Server: test-server
|
||||
Tool: test-tool
|
||||
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' > should show "allow always" when folder is trusted 1`] = `
|
||||
"MCP Server: test-server
|
||||
Tool: test-tool
|
||||
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Yes, allow once
|
||||
2. Yes, always allow tool "test-tool" from server "test-server"
|
||||
3. Yes, always allow all tools from server "test-server"
|
||||
4. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,96 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ? for Confirming status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ - test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockAnsiOutput:hello │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockDiff:--- a/file.txt │
|
||||
│ +++ b/file.txt │
|
||||
│ @@ -1 +1 @@ │
|
||||
│ -old │
|
||||
│ +new │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders basic tool information 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing ← │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
`;
|
||||
@@ -0,0 +1,326 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolResultDisplay > falls back to plain text if availableHeight is set and not in alternate buffer 1`] = `"MaxSizedBox:Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"MarkdownDisplay: Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"AnsiOutputText: {"text":"ansi content"}"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"MarkdownDisplay: Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"MaxSizedBox:Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > truncates very long string results 1`] = `
|
||||
"MaxSizedBo...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
`;
|
||||
@@ -0,0 +1,20 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UserMessage > renders multiline user message 1`] = `
|
||||
"
|
||||
> Line 1
|
||||
Line 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > renders normal user message with correct prefix 1`] = `
|
||||
"
|
||||
> Hello Gemini
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > renders slash command message 1`] = `
|
||||
"
|
||||
> /help
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`WarningMessage > renders multiline warning messages 1`] = `
|
||||
"
|
||||
⚠ Warning line 1
|
||||
Warning line 2"
|
||||
`;
|
||||
|
||||
exports[`WarningMessage > renders with the correct prefix and text 1`] = `
|
||||
"
|
||||
⚠ Watch out!"
|
||||
`;
|
||||
Reference in New Issue
Block a user