mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
Fix tests to wrap all calls changing the UI with act. (#12268)
This commit is contained in:
@@ -11,7 +11,6 @@ export default defineConfig({
|
||||
test: {
|
||||
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
reporters: ['default', 'junit'],
|
||||
silent: true,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
|
||||
// The waitFor from vitest doesn't properly wrap in act(), so we have to
|
||||
// implement our own like the one in @testing-library/react
|
||||
// or @testing-library/react-native
|
||||
// The version of waitFor from vitest is still fine to use if you aren't waiting
|
||||
// for React state updates.
|
||||
export async function waitFor(
|
||||
assertion: () => void,
|
||||
{ timeout = 1000, interval = 50 } = {},
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
assertion();
|
||||
return;
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,37 @@
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { renderHook } from './render.js';
|
||||
import { Text } from 'ink';
|
||||
import { renderHook, render } from './render.js';
|
||||
import { waitFor } from './async.js';
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a component', () => {
|
||||
const { lastFrame } = render(<Text>Hello World</Text>);
|
||||
expect(lastFrame()).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should support rerender', () => {
|
||||
const { lastFrame, rerender } = render(<Text>Hello</Text>);
|
||||
expect(lastFrame()).toBe('Hello');
|
||||
|
||||
rerender(<Text>World</Text>);
|
||||
expect(lastFrame()).toBe('World');
|
||||
});
|
||||
|
||||
it('should support unmount', () => {
|
||||
const cleanup = vi.fn();
|
||||
function TestComponent() {
|
||||
useEffect(() => cleanup, []);
|
||||
return <Text>Hello</Text>;
|
||||
}
|
||||
|
||||
const { unmount } = render(<TestComponent />);
|
||||
unmount();
|
||||
|
||||
expect(cleanup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderHook', () => {
|
||||
it('should rerender with previous props when called without arguments', async () => {
|
||||
@@ -23,19 +53,19 @@ describe('renderHook', () => {
|
||||
});
|
||||
|
||||
expect(result.current.value).toBe(1);
|
||||
await vi.waitFor(() => expect(result.current.count).toBe(1));
|
||||
await waitFor(() => expect(result.current.count).toBe(1));
|
||||
|
||||
// Rerender with new props
|
||||
rerender({ value: 2 });
|
||||
expect(result.current.value).toBe(2);
|
||||
await vi.waitFor(() => expect(result.current.count).toBe(2));
|
||||
await waitFor(() => expect(result.current.count).toBe(2));
|
||||
|
||||
// Rerender without arguments should use previous props (value: 2)
|
||||
// This would previously crash or pass undefined if not fixed
|
||||
rerender();
|
||||
expect(result.current.value).toBe(2);
|
||||
// Count should not increase because value didn't change
|
||||
await vi.waitFor(() => expect(result.current.count).toBe(2));
|
||||
await waitFor(() => expect(result.current.count).toBe(2));
|
||||
});
|
||||
|
||||
it('should handle initial render without props', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render as inkRender } from 'ink-testing-library';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { LoadedSettings, type Settings } from '../config/settings.js';
|
||||
@@ -19,6 +19,34 @@ import { VimModeProvider } from '../ui/contexts/VimModeContext.js';
|
||||
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
|
||||
// Wrapper around ink-testing-library's render that ensures act() is called
|
||||
export const render = (
|
||||
tree: React.ReactElement,
|
||||
): ReturnType<typeof inkRender> => {
|
||||
let renderResult: ReturnType<typeof inkRender> =
|
||||
undefined as unknown as ReturnType<typeof inkRender>;
|
||||
act(() => {
|
||||
renderResult = inkRender(tree);
|
||||
});
|
||||
|
||||
const originalUnmount = renderResult.unmount;
|
||||
const originalRerender = renderResult.rerender;
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
originalUnmount();
|
||||
});
|
||||
},
|
||||
rerender: (newTree: React.ReactElement) => {
|
||||
act(() => {
|
||||
originalRerender(newTree);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mockConfig = {
|
||||
getModel: () => 'gemini-pro',
|
||||
getTargetDir: () =>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../test-utils/render.js';
|
||||
import { Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
import { App } from './App.js';
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
type Mock,
|
||||
type MockedObject,
|
||||
} from 'vitest';
|
||||
import { render, cleanup } from 'ink-testing-library';
|
||||
import { render } from '../test-utils/render.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { act, useContext } from 'react';
|
||||
import { AppContainer } from './AppContainer.js';
|
||||
import {
|
||||
type Config,
|
||||
@@ -47,7 +49,6 @@ import {
|
||||
UIActionsContext,
|
||||
type UIActions,
|
||||
} from './contexts/UIActionsContext.js';
|
||||
import { useContext } from 'react';
|
||||
|
||||
// Mock useStdout to capture terminal title writes
|
||||
let mockStdout: { write: ReturnType<typeof vi.fn> };
|
||||
@@ -323,53 +324,59 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
it('renders without crashing with minimal props', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
it('renders without crashing with minimal props', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with startup warnings', () => {
|
||||
it('renders with startup warnings', async () => {
|
||||
const startupWarnings = ['Warning 1', 'Warning 2'];
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
startupWarnings={startupWarnings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
startupWarnings={startupWarnings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Initialization', () => {
|
||||
it('initializes with theme error from initialization result', () => {
|
||||
it('initializes with theme error from initialization result', async () => {
|
||||
const initResultWithError = {
|
||||
...mockInitResult,
|
||||
themeError: 'Failed to load theme',
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={initResultWithError}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={initResultWithError}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles debug mode state', () => {
|
||||
@@ -390,7 +397,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('Context Providers', () => {
|
||||
it('provides AppContext with correct values', () => {
|
||||
it('provides AppContext with correct values', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -399,53 +406,62 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Should render and unmount cleanly
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it('provides UIStateContext with state management', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
it('provides UIStateContext with state management', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('provides UIActionsContext with action handlers', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
it('provides UIActionsContext with action handlers', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('provides ConfigContext with config object', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
it('provides ConfigContext with config object', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Settings Integration', () => {
|
||||
it('handles settings with all display options disabled', () => {
|
||||
it('handles settings with all display options disabled', async () => {
|
||||
const settingsAllHidden = {
|
||||
merged: {
|
||||
hideBanner: true,
|
||||
@@ -455,19 +471,21 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={settingsAllHidden}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={settingsAllHidden}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles settings with memory usage enabled', () => {
|
||||
it('handles settings with memory usage enabled', async () => {
|
||||
const settingsWithMemory = {
|
||||
merged: {
|
||||
hideBanner: false,
|
||||
@@ -477,72 +495,80 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={settingsWithMemory}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={settingsWithMemory}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version Handling', () => {
|
||||
it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])(
|
||||
'handles version format: %s',
|
||||
(version) => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version={version}
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
async (version) => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version={version}
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('handles config methods that might throw', () => {
|
||||
it('handles config methods that might throw', async () => {
|
||||
const errorConfig = makeFakeConfig();
|
||||
vi.spyOn(errorConfig, 'getModel').mockImplementation(() => {
|
||||
throw new Error('Config error');
|
||||
});
|
||||
|
||||
// Should still render without crashing - errors should be handled internally
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={errorConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={errorConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles undefined settings gracefully', () => {
|
||||
it('handles undefined settings gracefully', async () => {
|
||||
const undefinedSettings = {
|
||||
merged: {},
|
||||
} as LoadedSettings;
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={undefinedSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={undefinedSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -564,9 +590,9 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('Quota and Fallback Integration', () => {
|
||||
it('passes a null proQuotaRequest to UIStateContext by default', () => {
|
||||
it('passes a null proQuotaRequest to UIStateContext by default', async () => {
|
||||
// The default mock from beforeEach already sets proQuotaRequest to null
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -574,12 +600,16 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Assert that the context value is as expected
|
||||
expect(capturedUIState.proQuotaRequest).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', () => {
|
||||
it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', async () => {
|
||||
// Arrange: Create a mock request object that a UI dialog would receive
|
||||
const mockRequest = {
|
||||
failedModel: 'gemini-pro',
|
||||
@@ -592,7 +622,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -600,12 +630,16 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Assert: The mock request is correctly passed through the context
|
||||
expect(capturedUIState.proQuotaRequest).toEqual(mockRequest);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes the handleProQuotaChoice function to UIActionsContext', () => {
|
||||
it('passes the handleProQuotaChoice function to UIActionsContext', async () => {
|
||||
// Arrange: Create a mock handler function
|
||||
const mockHandler = vi.fn();
|
||||
mockedUseQuotaAndFallback.mockReturnValue({
|
||||
@@ -614,7 +648,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -622,13 +656,19 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Assert: The action in the context is the mock handler we provided
|
||||
expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler);
|
||||
|
||||
// You can even verify that the plumbed function is callable
|
||||
capturedUIActions.handleProQuotaChoice('auth');
|
||||
act(() => {
|
||||
capturedUIActions.handleProQuotaChoice('auth');
|
||||
});
|
||||
expect(mockHandler).toHaveBeenCalledWith('auth');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -993,7 +1033,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('should set and clear the queue error message after a timeout', async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender, unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1001,10 +1041,15 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(0);
|
||||
});
|
||||
|
||||
expect(capturedUIState.queueErrorMessage).toBeNull();
|
||||
|
||||
capturedUIActions.setQueueErrorMessage('Test error');
|
||||
act(() => {
|
||||
capturedUIActions.setQueueErrorMessage('Test error');
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1015,7 +1060,9 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
expect(capturedUIState.queueErrorMessage).toBe('Test error');
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3000);
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1025,10 +1072,11 @@ describe('AppContainer State Management', () => {
|
||||
/>,
|
||||
);
|
||||
expect(capturedUIState.queueErrorMessage).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset the timer if a new error message is set', async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender, unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1036,8 +1084,13 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(0);
|
||||
});
|
||||
|
||||
capturedUIActions.setQueueErrorMessage('First error');
|
||||
act(() => {
|
||||
capturedUIActions.setQueueErrorMessage('First error');
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1048,9 +1101,13 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
expect(capturedUIState.queueErrorMessage).toBe('First error');
|
||||
|
||||
vi.advanceTimersByTime(1500);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1500);
|
||||
});
|
||||
|
||||
capturedUIActions.setQueueErrorMessage('Second error');
|
||||
act(() => {
|
||||
capturedUIActions.setQueueErrorMessage('Second error');
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1061,7 +1118,9 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
expect(capturedUIState.queueErrorMessage).toBe('Second error');
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2000);
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1073,7 +1132,9 @@ describe('AppContainer State Management', () => {
|
||||
expect(capturedUIState.queueErrorMessage).toBe('Second error');
|
||||
|
||||
// 5. Advance time past the 3 second timeout from the second message
|
||||
vi.advanceTimersByTime(1000);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1083,6 +1144,7 @@ describe('AppContainer State Management', () => {
|
||||
/>,
|
||||
);
|
||||
expect(capturedUIState.queueErrorMessage).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1090,7 +1152,7 @@ describe('AppContainer State Management', () => {
|
||||
const mockedMeasureElement = measureElement as Mock;
|
||||
const mockedUseTerminalSize = useTerminalSize as Mock;
|
||||
|
||||
it('should prevent terminal height from being less than 1', () => {
|
||||
it('should prevent terminal height from being less than 1', async () => {
|
||||
const resizePtySpy = vi.spyOn(ShellExecutionService, 'resizePty');
|
||||
// Arrange: Simulate a small terminal and a large footer
|
||||
mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 });
|
||||
@@ -1106,7 +1168,7 @@ describe('AppContainer State Management', () => {
|
||||
activePtyId: 'some-id',
|
||||
});
|
||||
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1114,6 +1176,9 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Assert: The shell should be resized to a minimum height of 1, not a negative number.
|
||||
// The old code would have tried to set a negative height.
|
||||
@@ -1122,6 +1187,7 @@ describe('AppContainer State Management', () => {
|
||||
resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1];
|
||||
// Check the height argument specifically
|
||||
expect(lastCall[2]).toBe(1);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1130,10 +1196,11 @@ describe('AppContainer State Management', () => {
|
||||
let mockHandleSlashCommand: Mock;
|
||||
let mockCancelOngoingRequest: Mock;
|
||||
let rerender: () => void;
|
||||
let unmount: () => void;
|
||||
|
||||
// Helper function to reduce boilerplate in tests
|
||||
const setupKeypressTest = () => {
|
||||
const { rerender: inkRerender } = render(
|
||||
const setupKeypressTest = async () => {
|
||||
const renderResult = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1141,9 +1208,12 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(0);
|
||||
});
|
||||
|
||||
rerender = () =>
|
||||
inkRerender(
|
||||
renderResult.rerender(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1151,17 +1221,20 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
unmount = renderResult.unmount;
|
||||
};
|
||||
|
||||
const pressKey = (key: Partial<Key>, times = 1) => {
|
||||
for (let i = 0; i < times; i++) {
|
||||
handleGlobalKeypress({
|
||||
name: 'c',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
...key,
|
||||
} as Key);
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 'c',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
...key,
|
||||
} as Key);
|
||||
});
|
||||
rerender();
|
||||
}
|
||||
};
|
||||
@@ -1208,7 +1281,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('CTRL+C', () => {
|
||||
it('should cancel ongoing request on first press', () => {
|
||||
it('should cancel ongoing request on first press', async () => {
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
@@ -1217,136 +1290,91 @@ describe('AppContainer State Management', () => {
|
||||
thought: null,
|
||||
cancelOngoingRequest: mockCancelOngoingRequest,
|
||||
});
|
||||
setupKeypressTest();
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
|
||||
expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should quit on second press', () => {
|
||||
setupKeypressTest();
|
||||
it('should quit on second press', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'c', ctrl: true }, 2);
|
||||
|
||||
expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset press count after a timeout', () => {
|
||||
setupKeypressTest();
|
||||
it('should reset press count after a timeout', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
vi.advanceTimersByTime(1001);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
});
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CTRL+D', () => {
|
||||
it('should do nothing if text buffer is not empty', () => {
|
||||
it('should do nothing if text buffer is not empty', async () => {
|
||||
mockedUseTextBuffer.mockReturnValue({
|
||||
text: 'some text',
|
||||
setText: vi.fn(),
|
||||
});
|
||||
setupKeypressTest();
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'd', ctrl: true }, 2);
|
||||
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should quit on second press if buffer is empty', () => {
|
||||
setupKeypressTest();
|
||||
it('should quit on second press if buffer is empty', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'd', ctrl: true }, 2);
|
||||
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset press count after a timeout', () => {
|
||||
setupKeypressTest();
|
||||
it('should reset press count after a timeout', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
vi.advanceTimersByTime(1001);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
});
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model Dialog Integration', () => {
|
||||
it('should provide isModelDialogOpen in the UIStateContext', () => {
|
||||
it('should provide isModelDialogOpen in the UIStateContext', async () => {
|
||||
mockedUseModelCommand.mockReturnValue({
|
||||
isModelDialogOpen: true,
|
||||
openModelDialog: vi.fn(),
|
||||
closeModelDialog: vi.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(capturedUIState.isModelDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should provide model dialog actions in the UIActionsContext', () => {
|
||||
const mockCloseModelDialog = vi.fn();
|
||||
|
||||
mockedUseModelCommand.mockReturnValue({
|
||||
isModelDialogOpen: false,
|
||||
openModelDialog: vi.fn(),
|
||||
closeModelDialog: mockCloseModelDialog,
|
||||
});
|
||||
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Verify that the actions are correctly passed through context
|
||||
capturedUIActions.closeModelDialog();
|
||||
expect(mockCloseModelDialog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreEvents Integration', () => {
|
||||
it('subscribes to UserFeedback and drains backlog on mount', () => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mockCoreEvents.on).toHaveBeenCalledWith(
|
||||
CoreEvent.UserFeedback,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCoreEvents.drainFeedbackBacklog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unsubscribes from UserFeedback on unmount', () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
@@ -1355,6 +1383,78 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(capturedUIState.isModelDialogOpen).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should provide model dialog actions in the UIActionsContext', async () => {
|
||||
const mockCloseModelDialog = vi.fn();
|
||||
|
||||
mockedUseModelCommand.mockReturnValue({
|
||||
isModelDialogOpen: false,
|
||||
openModelDialog: vi.fn(),
|
||||
closeModelDialog: mockCloseModelDialog,
|
||||
});
|
||||
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Verify that the actions are correctly passed through context
|
||||
act(() => {
|
||||
capturedUIActions.closeModelDialog();
|
||||
});
|
||||
expect(mockCloseModelDialog).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreEvents Integration', () => {
|
||||
it('subscribes to UserFeedback and drains backlog on mount', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockCoreEvents.on).toHaveBeenCalledWith(
|
||||
CoreEvent.UserFeedback,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCoreEvents.drainFeedbackBacklog).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('unsubscribes from UserFeedback on unmount', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -1364,8 +1464,8 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('adds history item when UserFeedback event is received', () => {
|
||||
render(
|
||||
it('adds history item when UserFeedback event is received', async () => {
|
||||
const { unmount } = render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={mockSettings}
|
||||
@@ -1373,6 +1473,9 @@ describe('AppContainer State Management', () => {
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Get the registered handler
|
||||
const handler = mockCoreEvents.on.mock.calls.find(
|
||||
@@ -1385,7 +1488,9 @@ describe('AppContainer State Management', () => {
|
||||
severity: 'error',
|
||||
message: 'Test error message',
|
||||
};
|
||||
handler(payload);
|
||||
act(() => {
|
||||
handler(payload);
|
||||
});
|
||||
|
||||
expect(mockedUseHistory().addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1394,6 +1499,7 @@ describe('AppContainer State Management', () => {
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { ApiAuthDialog } from './ApiAuthDialog.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { AnsiOutputText } from './AnsiOutput.js';
|
||||
import type { AnsiOutput, AnsiToken } from '@google/gemini-cli-core';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { Composer } from './Composer.js';
|
||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import { Text } from 'ink';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { ConsentPrompt } from './ConsentPrompt.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
|
||||
@@ -32,7 +33,7 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('renders a string prompt with MarkdownDisplay', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
@@ -48,11 +49,12 @@ describe('ConsentPrompt', () => {
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders a ReactNode prompt directly', () => {
|
||||
const prompt = <Text>Are you sure?</Text>;
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
@@ -62,11 +64,12 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
expect(MockedMarkdownDisplay).not.toHaveBeenCalled();
|
||||
expect(lastFrame()).toContain('Are you sure?');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onConfirm with true when "Yes" is selected', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
@@ -75,14 +78,17 @@ describe('ConsentPrompt', () => {
|
||||
);
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelect(true);
|
||||
act(() => {
|
||||
onSelect(true);
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onConfirm with false when "No" is selected', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
@@ -91,14 +97,17 @@ describe('ConsentPrompt', () => {
|
||||
);
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelect(false);
|
||||
act(() => {
|
||||
onSelect(false);
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes correct items to RadioButtonSelect', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
@@ -115,5 +124,6 @@ describe('ConsentPrompt', () => {
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
@@ -37,17 +37,18 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
};
|
||||
|
||||
it('should render on a single line on a wide screen', () => {
|
||||
const { lastFrame } = renderWithWidth(120, baseProps);
|
||||
const { lastFrame, unmount } = renderWithWidth(120, baseProps);
|
||||
const output = lastFrame()!;
|
||||
expect(output).toContain(
|
||||
'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file | 1 MCP server',
|
||||
);
|
||||
// Check for absence of newlines
|
||||
expect(output.includes('\n')).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render on multiple lines on a narrow screen', () => {
|
||||
const { lastFrame } = renderWithWidth(60, baseProps);
|
||||
const { lastFrame, unmount } = renderWithWidth(60, baseProps);
|
||||
const output = lastFrame()!;
|
||||
const expectedLines = [
|
||||
' Using:',
|
||||
@@ -57,17 +58,26 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
];
|
||||
const actualLines = output.split('\n');
|
||||
expect(actualLines).toEqual(expectedLines);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should switch layout at the 80-column breakpoint', () => {
|
||||
// At 80 columns, should be on one line
|
||||
const { lastFrame: wideFrame } = renderWithWidth(80, baseProps);
|
||||
const { lastFrame: wideFrame, unmount: unmountWide } = renderWithWidth(
|
||||
80,
|
||||
baseProps,
|
||||
);
|
||||
expect(wideFrame()!.includes('\n')).toBe(false);
|
||||
unmountWide();
|
||||
|
||||
// At 79 columns, should be on multiple lines
|
||||
const { lastFrame: narrowFrame } = renderWithWidth(79, baseProps);
|
||||
const { lastFrame: narrowFrame, unmount: unmountNarrow } = renderWithWidth(
|
||||
79,
|
||||
baseProps,
|
||||
);
|
||||
expect(narrowFrame()!.includes('\n')).toBe(true);
|
||||
expect(narrowFrame()!.split('\n').length).toBe(4);
|
||||
unmountNarrow();
|
||||
});
|
||||
|
||||
it('should not render empty parts', () => {
|
||||
@@ -77,9 +87,10 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
contextFileNames: [],
|
||||
mcpServers: {},
|
||||
};
|
||||
const { lastFrame } = renderWithWidth(60, props);
|
||||
const { lastFrame, unmount } = renderWithWidth(60, props);
|
||||
const expectedLines = [' Using:', ' - 1 open file (ctrl+g to view)'];
|
||||
const actualLines = lastFrame()!.split('\n');
|
||||
expect(actualLines).toEqual(expectedLines);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { act } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { FolderTrustDialog } from './FolderTrustDialog.js';
|
||||
@@ -54,12 +55,12 @@ describe('FolderTrustDialog', () => {
|
||||
stdin.write('\u001b[27u'); // Press kitty escape key
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
'A folder trust level must be selected to continue. Exiting since escape was pressed.',
|
||||
);
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
@@ -93,7 +94,7 @@ describe('FolderTrustDialog', () => {
|
||||
stdin.write('r');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedExit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Header } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
@@ -20,25 +20,35 @@ describe('<Header />', () => {
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<Header version="1.0.0" nightly={false} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(longAsciiLogo);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(customArt);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays the version number when nightly is true', () => {
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={true} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<Header version="1.0.0" nightly={true} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('v1.0.0');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not display the version number when nightly is false', () => {
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<Header version="1.0.0" nightly={false} />,
|
||||
);
|
||||
expect(lastFrame()).not.toContain('v1.0.0');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Help } from './Help.js';
|
||||
import type { SlashCommand } from '../commands/types.js';
|
||||
@@ -44,18 +44,20 @@ const mockCommands: readonly SlashCommand[] = [
|
||||
|
||||
describe('Help Component', () => {
|
||||
it('should not render hidden commands', () => {
|
||||
const { lastFrame } = render(<Help commands={mockCommands} />);
|
||||
const { lastFrame, unmount } = render(<Help commands={mockCommands} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('/test');
|
||||
expect(output).not.toContain('/hidden');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render hidden subcommands', () => {
|
||||
const { lastFrame } = render(<Help commands={mockCommands} />);
|
||||
const { lastFrame, unmount } = render(<Help commands={mockCommands} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('visible-child');
|
||||
expect(output).not.toContain('hidden-child');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { act } from 'react';
|
||||
import type { InputPromptProps } from './InputPrompt.js';
|
||||
import { InputPrompt } from './InputPrompt.js';
|
||||
@@ -240,7 +241,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(),
|
||||
);
|
||||
unmount();
|
||||
@@ -252,10 +253,10 @@ describe('InputPrompt', () => {
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B');
|
||||
await waitFor(() =>
|
||||
expect(mockShellHistory.getNextCommand).toHaveBeenCalled(),
|
||||
);
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(mockShellHistory.getNextCommand).toHaveBeenCalled(),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -269,7 +270,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
|
||||
});
|
||||
@@ -284,7 +285,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith(
|
||||
'ls -l',
|
||||
);
|
||||
@@ -300,21 +301,19 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
|
||||
);
|
||||
await waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockInputHistory.navigateDown).toHaveBeenCalled(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('some text'),
|
||||
);
|
||||
|
||||
@@ -342,14 +341,14 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u0010'); // Ctrl+P
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
|
||||
@@ -374,14 +373,14 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u000E'); // Ctrl+N
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
|
||||
@@ -395,32 +394,29 @@ describe('InputPrompt', () => {
|
||||
showSuggestions: false,
|
||||
});
|
||||
props.buffer.setText('some text');
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
|
||||
);
|
||||
await waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockInputHistory.navigateDown).toHaveBeenCalled(),
|
||||
);
|
||||
await act(async () => {
|
||||
stdin.write('\u0010'); // Ctrl+P
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
await act(async () => {
|
||||
stdin.write('\u000E'); // Ctrl+N
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
|
||||
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
|
||||
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
|
||||
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -447,7 +443,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
|
||||
expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(
|
||||
props.config.getTargetDir(),
|
||||
@@ -470,7 +466,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
|
||||
});
|
||||
expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled();
|
||||
@@ -489,7 +485,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockBuffer.setText).not.toHaveBeenCalled();
|
||||
@@ -518,7 +514,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Should insert at cursor position with spaces
|
||||
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
|
||||
});
|
||||
@@ -549,7 +545,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error handling clipboard image:',
|
||||
expect.any(Error),
|
||||
@@ -577,7 +573,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
unmount();
|
||||
@@ -601,7 +597,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1),
|
||||
);
|
||||
unmount();
|
||||
@@ -626,7 +622,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
// It should NOT become '/show'. It should correctly become '/memory show'.
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
@@ -648,7 +644,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
unmount();
|
||||
@@ -668,7 +664,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// The app should autocomplete the text, NOT submit.
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
});
|
||||
@@ -700,7 +696,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab for autocomplete
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
unmount();
|
||||
@@ -714,9 +710,10 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Press Enter
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -733,9 +730,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/clear'),
|
||||
);
|
||||
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -752,9 +747,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/clear'),
|
||||
);
|
||||
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -772,7 +765,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
@@ -790,7 +783,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.backspace).toHaveBeenCalled();
|
||||
expect(props.buffer.newline).toHaveBeenCalled();
|
||||
});
|
||||
@@ -800,13 +793,15 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
it('should clear the buffer on Ctrl+C if it has text', async () => {
|
||||
props.buffer.setText('some text to clear');
|
||||
await act(async () => {
|
||||
props.buffer.setText('some text to clear');
|
||||
});
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x03'); // Ctrl+C character
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('');
|
||||
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
|
||||
});
|
||||
@@ -821,9 +816,10 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x03'); // Ctrl+C character
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
|
||||
expect(props.buffer.setText).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.setText).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -922,7 +918,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
@@ -949,7 +945,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
@@ -965,7 +961,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
});
|
||||
@@ -982,7 +978,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1000,7 +996,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x1B[200~pasted text\x1B[201~');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paste: true,
|
||||
@@ -1020,7 +1016,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('a');
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
await waitFor(() => {});
|
||||
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
@@ -1090,7 +1086,7 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
});
|
||||
@@ -1147,7 +1143,7 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
});
|
||||
@@ -1171,7 +1167,7 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const lines = frame!.split('\n');
|
||||
// The line with the cursor should just be an inverted space inside the box border
|
||||
@@ -1203,7 +1199,7 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
// Check that all lines, including the empty one, are rendered.
|
||||
// This implicitly tests that the Box wrapper provides height for the empty line.
|
||||
@@ -1241,7 +1237,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write(`\x1b[200~${pastedText}\x1b[201~`);
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Verify that the buffer's handleInput was called once with the full text
|
||||
expect(props.buffer.handleInput).toHaveBeenCalledTimes(1);
|
||||
expect(props.buffer.handleInput).toHaveBeenCalledWith(
|
||||
@@ -1277,7 +1273,9 @@ describe('InputPrompt', () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Simulate a paste operation (this should set the paste protection)
|
||||
await act(async () => {
|
||||
@@ -1288,7 +1286,9 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Verify that onSubmit was NOT called due to recent paste protection
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
@@ -1304,13 +1304,17 @@ describe('InputPrompt', () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Simulate a paste operation (this sets the protection)
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[200~pasted text\x1b[201~');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Advance timers past the protection timeout
|
||||
await act(async () => {
|
||||
@@ -1321,7 +1325,9 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('pasted text');
|
||||
expect(props.buffer.newline).not.toHaveBeenCalled();
|
||||
@@ -1349,19 +1355,25 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
{ kittyProtocolEnabled: true },
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Simulate a paste operation
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[200~some pasted stuff\x1b[201~');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Simulate an Enter key press immediately after paste
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Verify that onSubmit was called
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('pasted command');
|
||||
@@ -1376,13 +1388,17 @@ describe('InputPrompt', () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Press Enter without any recent paste
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Verify that onSubmit was called normally
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('normal command');
|
||||
@@ -1404,17 +1420,17 @@ describe('InputPrompt', () => {
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(true);
|
||||
await waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('');
|
||||
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('');
|
||||
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
@@ -1431,18 +1447,16 @@ describe('InputPrompt', () => {
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(true);
|
||||
await waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('a');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
|
||||
await waitFor(() => {
|
||||
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
@@ -1457,10 +1471,10 @@ describe('InputPrompt', () => {
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
await waitFor(() =>
|
||||
expect(props.setShellModeActive).toHaveBeenCalledWith(false),
|
||||
);
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(props.setShellModeActive).toHaveBeenCalledWith(false),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -1479,7 +1493,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(),
|
||||
);
|
||||
unmount();
|
||||
@@ -1494,12 +1508,16 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
{ kittyProtocolEnabled: false },
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
@@ -1514,12 +1532,12 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x0C');
|
||||
});
|
||||
await vi.waitFor(() => expect(props.onClearScreen).toHaveBeenCalled());
|
||||
await waitFor(() => expect(props.onClearScreen).toHaveBeenCalled());
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x01');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(props.buffer.move).toHaveBeenCalledWith('home'),
|
||||
);
|
||||
unmount();
|
||||
@@ -1561,7 +1579,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain('(r:)');
|
||||
expect(frame).toContain('echo hello');
|
||||
@@ -1580,7 +1598,6 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
await vi.waitFor(() => {});
|
||||
await act(async () => {
|
||||
stdin.write('\x1B');
|
||||
});
|
||||
@@ -1588,12 +1605,11 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\u001b[27u'); // Press kitty escape key
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain('(r:)');
|
||||
expect(stdout.lastFrame()).not.toContain('echo hello');
|
||||
});
|
||||
|
||||
expect(stdout.lastFrame()).not.toContain('echo hello');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -1629,7 +1645,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// Verify reverse search is active
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('(r:)');
|
||||
});
|
||||
|
||||
@@ -1637,7 +1653,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('echo hello');
|
||||
});
|
||||
@@ -1665,7 +1681,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('(r:)');
|
||||
});
|
||||
|
||||
@@ -1673,7 +1689,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain('(r:)');
|
||||
});
|
||||
|
||||
@@ -1708,7 +1724,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('(r:)');
|
||||
});
|
||||
|
||||
@@ -1717,7 +1733,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\u001b[27u');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain('(r:)');
|
||||
expect(props.buffer.text).toBe(initialText);
|
||||
expect(props.buffer.cursor).toEqual(initialCursor);
|
||||
@@ -1740,7 +1756,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x05'); // Ctrl+E
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.move).toHaveBeenCalledWith('end');
|
||||
});
|
||||
expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
|
||||
@@ -1759,7 +1775,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x05'); // Ctrl+E
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(props.buffer.move).toHaveBeenCalledWith('end');
|
||||
});
|
||||
expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
|
||||
@@ -1793,7 +1809,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\x12'); // Ctrl+R
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame() ?? '';
|
||||
expect(frame).toContain('(r:)');
|
||||
expect(frame).toContain('git commit');
|
||||
@@ -1822,14 +1838,14 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clean(stdout.lastFrame())).toContain('→');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[C');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clean(stdout.lastFrame())).toContain('←');
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
@@ -1839,7 +1855,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[D');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(clean(stdout.lastFrame())).toContain('→');
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
@@ -1871,7 +1887,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-collapsed-match',
|
||||
);
|
||||
@@ -1880,7 +1896,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[C');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toMatchSnapshot(
|
||||
'command-search-render-expanded-match',
|
||||
);
|
||||
@@ -1909,7 +1925,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\x12');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const frame = clean(stdout.lastFrame());
|
||||
// Ensure it rendered the search mode
|
||||
expect(frame).toContain('(r:)');
|
||||
@@ -1933,7 +1949,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
@@ -1957,7 +1973,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
|
||||
);
|
||||
expect(mockPopAllMessages).not.toHaveBeenCalled();
|
||||
@@ -1976,7 +1992,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
callback(undefined);
|
||||
@@ -2002,7 +2018,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2018,7 +2034,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -2041,7 +2057,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2056,7 +2072,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
|
||||
);
|
||||
unmount();
|
||||
@@ -2074,7 +2090,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[A');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -2094,7 +2110,7 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2103,7 +2119,7 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2112,7 +2128,7 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -2122,7 +2138,7 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
|
||||
// This snapshot is good to make sure there was an input prompt but does
|
||||
// not show the inverted cursor because snapshots do not show colors.
|
||||
@@ -2140,7 +2156,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('a');
|
||||
});
|
||||
await vi.waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled());
|
||||
unmount();
|
||||
});
|
||||
describe('command queuing while streaming', () => {
|
||||
@@ -2184,7 +2200,7 @@ describe('InputPrompt', () => {
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
if (shouldSubmit) {
|
||||
expect(props.onSubmit).toHaveBeenCalledWith(bufferText);
|
||||
expect(props.setQueueErrorMessage).not.toHaveBeenCalled();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { StreamingContext } from '../contexts/StreamingContext.js';
|
||||
@@ -96,11 +96,12 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Processing data...',
|
||||
elapsedTime: 3,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
expect(lastFrame()).toContain('Processing data...');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display the elapsedTime correctly when Responding', () => {
|
||||
@@ -108,11 +109,12 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Working...',
|
||||
elapsedTime: 60,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
expect(lastFrame()).toContain('(esc to cancel, 1m)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display the elapsedTime correctly in human-readable format', () => {
|
||||
@@ -120,24 +122,26 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Working...',
|
||||
elapsedTime: 125,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
expect(lastFrame()).toContain('(esc to cancel, 2m 5s)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render rightContent when provided', () => {
|
||||
const rightContent = <Text>Extra Info</Text>;
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} rightContent={rightContent} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
expect(lastFrame()).toContain('Extra Info');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should transition correctly between states using rerender', () => {
|
||||
const { lastFrame, rerender } = renderWithContext(
|
||||
const { lastFrame, rerender, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
@@ -179,6 +183,7 @@ describe('<LoadingIndicator />', () => {
|
||||
</StreamingContext.Provider>,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display fallback phrase if thought is empty', () => {
|
||||
@@ -187,12 +192,13 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Loading...',
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Loading...');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display the subject of a thought', () => {
|
||||
@@ -203,7 +209,7 @@ describe('<LoadingIndicator />', () => {
|
||||
},
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
@@ -213,6 +219,7 @@ describe('<LoadingIndicator />', () => {
|
||||
expect(output).toContain('Thinking about something...');
|
||||
expect(output).not.toContain('and other stuff.');
|
||||
}
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should prioritize thought.subject over currentLoadingPhrase', () => {
|
||||
@@ -224,17 +231,18 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'This should not be displayed',
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('This should be displayed');
|
||||
expect(output).not.toContain('This should not be displayed');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should truncate long primary text instead of wrapping', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
currentLoadingPhrase={
|
||||
@@ -246,11 +254,12 @@ describe('<LoadingIndicator />', () => {
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('responsive layout', () => {
|
||||
it('should render on a single line on a wide terminal', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
rightContent={<Text>Right</Text>}
|
||||
@@ -264,10 +273,11 @@ describe('<LoadingIndicator />', () => {
|
||||
expect(output).toContain('Loading...');
|
||||
expect(output).toContain('(esc to cancel, 5s)');
|
||||
expect(output).toContain('Right');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render on multiple lines on a narrow terminal', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
rightContent={<Text>Right</Text>}
|
||||
@@ -288,24 +298,27 @@ describe('<LoadingIndicator />', () => {
|
||||
expect(lines[1]).toContain('(esc to cancel, 5s)');
|
||||
expect(lines[2]).toContain('Right');
|
||||
}
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use wide layout at 80 columns', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Responding,
|
||||
80,
|
||||
);
|
||||
expect(lastFrame()?.includes('\n')).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use narrow layout at 79 columns', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, unmount } = renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Responding,
|
||||
79,
|
||||
);
|
||||
expect(lastFrame()?.includes('\n')).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render, cleanup } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
@@ -80,16 +81,17 @@ describe('<ModelDialog />', () => {
|
||||
});
|
||||
|
||||
it('renders the title and help text', () => {
|
||||
const { lastFrame } = renderComponent();
|
||||
const { lastFrame, unmount } = renderComponent();
|
||||
expect(lastFrame()).toContain('Select Model');
|
||||
expect(lastFrame()).toContain('(Press Esc to close)');
|
||||
expect(lastFrame()).toContain(
|
||||
'> To use a specific Gemini model, use the --model flag.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes all model options to DescriptiveRadioButtonSelect', () => {
|
||||
renderComponent();
|
||||
const { unmount } = renderComponent();
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
@@ -99,11 +101,12 @@ describe('<ModelDialog />', () => {
|
||||
expect(props.items[2].value).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(props.items[3].value).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
|
||||
expect(props.showNumbers).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('initializes with the model from ConfigContext', () => {
|
||||
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_FLASH_MODEL);
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
@@ -112,10 +115,11 @@ describe('<ModelDialog />', () => {
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('initializes with "auto" model if context is not provided', () => {
|
||||
renderComponent({}, undefined);
|
||||
const { unmount } = renderComponent({}, undefined);
|
||||
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -123,13 +127,14 @@ describe('<ModelDialog />', () => {
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('initializes with "auto" model if getModel returns undefined', () => {
|
||||
const mockGetModel = vi.fn(() => undefined);
|
||||
// @ts-expect-error This test validates component robustness when getModel
|
||||
// returns an unexpected undefined value.
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
|
||||
@@ -142,10 +147,11 @@ describe('<ModelDialog />', () => {
|
||||
undefined,
|
||||
);
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => {
|
||||
const { props, mockConfig } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
const { props, mockConfig, unmount } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
@@ -155,17 +161,19 @@ describe('<ModelDialog />', () => {
|
||||
// Assert against the default mock provided by renderComponent
|
||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => {
|
||||
renderComponent();
|
||||
const { unmount } = renderComponent();
|
||||
|
||||
const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight;
|
||||
expect(childOnHighlight).toBeUndefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onClose prop when "escape" key is pressed', () => {
|
||||
const { props } = renderComponent();
|
||||
const { props, unmount } = renderComponent();
|
||||
|
||||
expect(mockedUseKeypress).toHaveBeenCalled();
|
||||
|
||||
@@ -193,11 +201,12 @@ describe('<ModelDialog />', () => {
|
||||
sequence: '',
|
||||
});
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('updates initialIndex when config context changes', () => {
|
||||
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO);
|
||||
const { rerender } = render(
|
||||
const { rerender, unmount } = render(
|
||||
<ConfigContext.Provider
|
||||
value={{ getModel: mockGetModel } as unknown as Config}
|
||||
>
|
||||
@@ -219,5 +228,6 @@ describe('<ModelDialog />', () => {
|
||||
// Should be called at least twice: initial render + re-render after context change
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(2);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(3);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
import { TrustLevel } from '../../config/trustedFolders.js';
|
||||
import { act } from 'react';
|
||||
@@ -68,7 +69,7 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Modify Trust Level');
|
||||
expect(lastFrame()).toContain('Folder: /test/dir');
|
||||
expect(lastFrame()).toContain('Current Level: DO_NOT_TRUST');
|
||||
@@ -90,7 +91,7 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
'Note: This folder behaves as a trusted folder because one of the parent folders is trusted.',
|
||||
);
|
||||
@@ -112,7 +113,7 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
'Note: This folder behaves as a trusted folder because the connected IDE workspace is trusted.',
|
||||
);
|
||||
@@ -124,7 +125,7 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Trust this folder (dir)');
|
||||
expect(lastFrame()).toContain('Trust parent folder (test)');
|
||||
});
|
||||
@@ -136,13 +137,13 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
act(() => {
|
||||
stdin.write('\u001b[27u'); // Kitty escape key
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(onExit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -167,11 +168,11 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
act(() => stdin.write('r')); // Press 'r' to restart
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockCommitTrustLevelChange).toHaveBeenCalled();
|
||||
expect(mockRelaunchApp).toHaveBeenCalled();
|
||||
expect(onExit).toHaveBeenCalled();
|
||||
@@ -197,11 +198,11 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
act(() => stdin.write('\u001b[27u')); // Press kitty escape key
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockCommitTrustLevelChange).not.toHaveBeenCalled();
|
||||
expect(onExit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { PrepareLabel, MAX_WIDTH } from './PrepareLabel.js';
|
||||
|
||||
describe('PrepareLabel', () => {
|
||||
@@ -13,7 +13,7 @@ describe('PrepareLabel', () => {
|
||||
const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, '');
|
||||
|
||||
it('renders plain label when no match (short label)', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label="simple command"
|
||||
userInput=""
|
||||
@@ -23,11 +23,12 @@ describe('PrepareLabel', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates long label when collapsed and no match', () => {
|
||||
const long = 'x'.repeat(MAX_WIDTH + 25);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label={long}
|
||||
userInput=""
|
||||
@@ -40,11 +41,12 @@ describe('PrepareLabel', () => {
|
||||
expect(f.endsWith('...')).toBe(true);
|
||||
expect(f.length).toBe(MAX_WIDTH + 3);
|
||||
expect(out).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows full long label when expanded and no match', () => {
|
||||
const long = 'y'.repeat(MAX_WIDTH + 25);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label={long}
|
||||
userInput=""
|
||||
@@ -56,13 +58,14 @@ describe('PrepareLabel', () => {
|
||||
const f = flat(out);
|
||||
expect(f.length).toBe(long.length);
|
||||
expect(out).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('highlights matched substring when expanded (text only visible)', () => {
|
||||
const label = 'run: git commit -m "feat: add search"';
|
||||
const userInput = 'commit';
|
||||
const matchedIndex = label.indexOf(userInput);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label={label}
|
||||
userInput={userInput}
|
||||
@@ -72,6 +75,7 @@ describe('PrepareLabel', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('creates centered window around match when collapsed', () => {
|
||||
@@ -80,7 +84,7 @@ describe('PrepareLabel', () => {
|
||||
const suffix = '/and/then/some/more/components/'.repeat(3);
|
||||
const label = prefix + core + suffix;
|
||||
const matchedIndex = prefix.length;
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label={label}
|
||||
userInput={core}
|
||||
@@ -95,6 +99,7 @@ describe('PrepareLabel', () => {
|
||||
expect(f.startsWith('...')).toBe(true);
|
||||
expect(f.endsWith('...')).toBe(true);
|
||||
expect(out).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates match itself when match is very long', () => {
|
||||
@@ -103,7 +108,7 @@ describe('PrepareLabel', () => {
|
||||
const suffix = ' in this text';
|
||||
const label = prefix + core + suffix;
|
||||
const matchedIndex = prefix.length;
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<PrepareLabel
|
||||
label={label}
|
||||
userInput={core}
|
||||
@@ -119,5 +124,6 @@ describe('PrepareLabel', () => {
|
||||
expect(f.endsWith('...')).toBe(true);
|
||||
expect(f.length).toBe(MAX_WIDTH + 2);
|
||||
expect(out).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
@@ -20,7 +21,7 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
it('should render with correct title and options', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -49,11 +50,12 @@ describe('ProQuotaDialog', () => {
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call onChoice with "auth" when "Change auth" is selected', () => {
|
||||
const mockOnChoice = vi.fn();
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -65,14 +67,17 @@ describe('ProQuotaDialog', () => {
|
||||
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
|
||||
|
||||
// Simulate the selection
|
||||
onSelect('auth');
|
||||
act(() => {
|
||||
onSelect('auth');
|
||||
});
|
||||
|
||||
expect(mockOnChoice).toHaveBeenCalledWith('auth');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call onChoice with "continue" when "Continue with flash" is selected', () => {
|
||||
const mockOnChoice = vi.fn();
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -84,8 +89,11 @@ describe('ProQuotaDialog', () => {
|
||||
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
|
||||
|
||||
// Simulate the selection
|
||||
onSelect('continue');
|
||||
act(() => {
|
||||
onSelect('continue');
|
||||
});
|
||||
|
||||
expect(mockOnChoice).toHaveBeenCalledWith('continue');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,24 +5,28 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
|
||||
describe('QueuedMessageDisplay', () => {
|
||||
it('renders nothing when message queue is empty', () => {
|
||||
const { lastFrame } = render(<QueuedMessageDisplay messageQueue={[]} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<QueuedMessageDisplay messageQueue={[]} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays single queued message', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<QueuedMessageDisplay messageQueue={['First message']} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
expect(output).toContain('First message');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays multiple queued messages', () => {
|
||||
@@ -32,7 +36,7 @@ describe('QueuedMessageDisplay', () => {
|
||||
'Third queued message',
|
||||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
|
||||
@@ -41,6 +45,7 @@ describe('QueuedMessageDisplay', () => {
|
||||
expect(output).toContain('First queued message');
|
||||
expect(output).toContain('Second queued message');
|
||||
expect(output).toContain('Third queued message');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows overflow indicator when more than 3 messages are queued', () => {
|
||||
@@ -52,7 +57,7 @@ describe('QueuedMessageDisplay', () => {
|
||||
'Message 5',
|
||||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
|
||||
@@ -64,17 +69,19 @@ describe('QueuedMessageDisplay', () => {
|
||||
expect(output).toContain('... (+2 more)');
|
||||
expect(output).not.toContain('Message 4');
|
||||
expect(output).not.toContain('Message 5');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('normalizes whitespace in messages', () => {
|
||||
const messageQueue = ['Message with\tmultiple\n whitespace'];
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
expect(output).toContain('Message with multiple whitespace');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { SettingsDialog } from './SettingsDialog.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
@@ -321,7 +322,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(down as string);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Disable Auto Update');
|
||||
});
|
||||
|
||||
@@ -330,7 +331,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(up as string);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
@@ -348,7 +349,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(TerminalKeys.UP_ARROW);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Should wrap to last setting (without relying on exact bullet character)
|
||||
expect(lastFrame()).toContain('Codebase Investigator Max Num Turns');
|
||||
});
|
||||
@@ -367,7 +368,7 @@ describe('SettingsDialog', () => {
|
||||
const { stdin, unmount, lastFrame } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render and verify we're on Vim Mode (first setting)
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
@@ -375,7 +376,7 @@ describe('SettingsDialog', () => {
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Disable Auto Update');
|
||||
});
|
||||
|
||||
@@ -384,14 +385,14 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
});
|
||||
// Wait for the setting change to be processed
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
vi.mocked(saveModifiedSettings).mock.calls.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Wait for the mock to be called
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -439,7 +440,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -513,7 +514,7 @@ describe('SettingsDialog', () => {
|
||||
const { lastFrame, unmount } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
@@ -569,7 +570,7 @@ describe('SettingsDialog', () => {
|
||||
const { lastFrame, unmount } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
});
|
||||
|
||||
@@ -735,7 +736,7 @@ describe('SettingsDialog', () => {
|
||||
// Since we can't easily target specific settings, we test the general behavior
|
||||
|
||||
// Should not show restart prompt initially
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).not.toContain(
|
||||
'To see changes, Gemini CLI must be restarted',
|
||||
);
|
||||
@@ -836,7 +837,7 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
}
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
vi.mocked(saveModifiedSettings).mock.calls.length,
|
||||
).toBeGreaterThan(0);
|
||||
@@ -928,7 +929,7 @@ describe('SettingsDialog', () => {
|
||||
const { lastFrame, unmount } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
@@ -978,7 +979,7 @@ describe('SettingsDialog', () => {
|
||||
const { lastFrame, unmount } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
@@ -1096,7 +1097,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write('\u001B');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
|
||||
});
|
||||
|
||||
@@ -1198,8 +1199,11 @@ describe('SettingsDialog', () => {
|
||||
userSettings: {},
|
||||
systemSettings: {},
|
||||
workspaceSettings: {},
|
||||
stdinActions: (stdin: { write: (data: string) => void }) =>
|
||||
stdin.write('\t'),
|
||||
stdinActions: (stdin: { write: (data: string) => void }) => {
|
||||
act(() => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'accessibility settings enabled',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ThemeDialog } from './ThemeDialog.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
@@ -126,7 +127,7 @@ describe('ThemeDialog Snapshots', () => {
|
||||
stdin.write('\x1b');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockOnCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
|
||||
@@ -178,10 +178,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
||||
exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -200,10 +200,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ > Apply To │
|
||||
│ ● 1. User Settings │
|
||||
│ 2. Workspace Settings │
|
||||
│ 3. System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import type { CompressionDisplayProps } from './CompressionMessage.js';
|
||||
import { CompressionMessage } from './CompressionMessage.js';
|
||||
import { CompressionStatus } from '@google/gemini-cli-core';
|
||||
@@ -27,10 +27,11 @@ describe('<CompressionMessage />', () => {
|
||||
describe('pending state', () => {
|
||||
it('renders pending message when compression is in progress', () => {
|
||||
const props = createCompressionProps({ isPending: true });
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Compressing chat history');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,13 +43,14 @@ describe('<CompressionMessage />', () => {
|
||||
newTokenCount: 50,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).toContain(
|
||||
'Chat history compressed from 100 to 50 tokens.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders success message for large successful compressions', () => {
|
||||
@@ -57,14 +59,16 @@ describe('<CompressionMessage />', () => {
|
||||
{ original: 700000, new: 350000 }, // Very large compression
|
||||
];
|
||||
|
||||
testCases.forEach(({ original, new: newTokens }) => {
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
newTokenCount: newTokens,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
@@ -73,7 +77,8 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
expect(output).not.toContain('Skipping compression');
|
||||
expect(output).not.toContain('did not reduce size');
|
||||
});
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,13 +91,14 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).toContain(
|
||||
'Compression was not beneficial for this history size.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders skip message when token counts are equal', () => {
|
||||
@@ -103,12 +109,13 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(
|
||||
'Compression was not beneficial for this history size.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,18 +139,21 @@ describe('<CompressionMessage />', () => {
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ original, new: newTokens, expected }) => {
|
||||
for (const { original, new: newTokens, expected } of testCases) {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
newTokenCount: newTokens,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(expected);
|
||||
});
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows skip message for small histories when new tokens >= original tokens', () => {
|
||||
@@ -153,7 +163,7 @@ describe('<CompressionMessage />', () => {
|
||||
{ original: 49999, new: 50000 }, // Just under 50k threshold
|
||||
];
|
||||
|
||||
testCases.forEach(({ original, new: newTokens }) => {
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
@@ -161,14 +171,17 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(
|
||||
'Compression was not beneficial for this history size.',
|
||||
);
|
||||
expect(output).not.toContain('compressed from');
|
||||
});
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows compression failure message for large histories when new tokens >= original tokens', () => {
|
||||
@@ -178,7 +191,7 @@ describe('<CompressionMessage />', () => {
|
||||
{ original: 100000, new: 100000 }, // Large history, same count
|
||||
];
|
||||
|
||||
testCases.forEach(({ original, new: newTokens }) => {
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
@@ -186,13 +199,16 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame } = render(<CompressionMessage {...props} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('compression did not reduce size');
|
||||
expect(output).not.toContain('compressed from');
|
||||
expect(output).not.toContain('Compression was not beneficial');
|
||||
});
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import * as CodeColorizer from '../../utils/CodeColorizer.js';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Box } from 'ink';
|
||||
import { TodoTray } from './Todo.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import type React from 'react';
|
||||
@@ -98,10 +98,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
const toolCalls = [createToolCall()];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiple tool calls with different statuses', () => {
|
||||
@@ -125,10 +126,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Error,
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders tool call awaiting confirmation', () => {
|
||||
@@ -146,10 +148,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders shell command with yellow border', () => {
|
||||
@@ -161,10 +164,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Success,
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders mixed tool calls including shell command', () => {
|
||||
@@ -188,10 +192,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Pending,
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with limited terminal height', () => {
|
||||
@@ -210,7 +215,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: 'More output here',
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
@@ -218,11 +223,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders when not focused', () => {
|
||||
const toolCalls = [createToolCall()];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
@@ -230,6 +236,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with narrow terminal width', () => {
|
||||
@@ -240,7 +247,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
'This is a very long description that might cause wrapping issues',
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
@@ -248,24 +255,27 @@ describe('<ToolGroupMessage />', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders empty tool calls array', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={[]} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border when tools are pending', () => {
|
||||
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
// The snapshot will capture the visual appearance including border color
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses yellow border for shell commands even when successful', () => {
|
||||
@@ -275,10 +285,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Success,
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses gray border when all tools are successful and no shell commands', () => {
|
||||
@@ -290,10 +301,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Success,
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,7 +325,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: '', // No result
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
@@ -321,6 +333,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -350,11 +363,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
// Should only show confirmation for the first tool
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import {
|
||||
BaseSelectionList,
|
||||
type BaseSelectionListProps,
|
||||
@@ -298,7 +299,7 @@ describe('BaseSelectionList', () => {
|
||||
|
||||
rerender(<BaseSelectionList {...componentProps} />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toBeTruthy();
|
||||
});
|
||||
};
|
||||
@@ -322,7 +323,7 @@ describe('BaseSelectionList', () => {
|
||||
// New visible window should be Items 2, 3, 4 (scroll offset 1).
|
||||
await updateActiveIndex(3);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Item 1');
|
||||
expect(output).toContain('Item 2');
|
||||
@@ -336,7 +337,7 @@ describe('BaseSelectionList', () => {
|
||||
|
||||
await updateActiveIndex(4);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
|
||||
expect(output).toContain('Item 5');
|
||||
@@ -347,7 +348,7 @@ describe('BaseSelectionList', () => {
|
||||
// This should trigger scroll up to show items 2, 3, 4
|
||||
await updateActiveIndex(1);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
@@ -361,7 +362,7 @@ describe('BaseSelectionList', () => {
|
||||
// Visible items: 8, 9, 10.
|
||||
const { lastFrame } = renderScrollableList(9);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 10');
|
||||
expect(output).toContain('Item 8');
|
||||
@@ -380,14 +381,14 @@ describe('BaseSelectionList', () => {
|
||||
expect(lastFrame()).toContain('Item 1');
|
||||
|
||||
await updateActiveIndex(3); // Should trigger scroll
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 1');
|
||||
});
|
||||
await updateActiveIndex(5); // Scroll further
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).toContain('Item 6');
|
||||
@@ -414,7 +415,7 @@ describe('BaseSelectionList', () => {
|
||||
it('should correctly identify the selected item when scrolled (high index)', async () => {
|
||||
renderScrollableList(5);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Item 6 (index 5) should be selected
|
||||
expect(mockRenderItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: 'Item 6' }),
|
||||
@@ -472,7 +473,7 @@ describe('BaseSelectionList', () => {
|
||||
0,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// At the top, should show first 3 items
|
||||
expect(output).toContain('Item 1');
|
||||
@@ -490,7 +491,7 @@ describe('BaseSelectionList', () => {
|
||||
5,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// After scrolling to middle, should see items around index 5
|
||||
expect(output).toContain('Item 4');
|
||||
@@ -509,7 +510,7 @@ describe('BaseSelectionList', () => {
|
||||
9,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// At the end, should show last 3 items
|
||||
expect(output).toContain('Item 8');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
|
||||
import { Box, Text } from 'ink';
|
||||
@@ -18,7 +18,7 @@ describe('<MaxSizedBox />', () => {
|
||||
setMaxSizedBoxDebugging(true);
|
||||
|
||||
it('renders children without truncation when they fit', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
@@ -28,10 +28,11 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals('Hello, World!');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides lines when content exceeds maxHeight', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -48,10 +49,11 @@ describe('<MaxSizedBox />', () => {
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
@@ -68,10 +70,11 @@ Line 3`);
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text that exceeds maxWidth', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={10} maxHeight={5}>
|
||||
<Box>
|
||||
@@ -84,13 +87,14 @@ Line 3`);
|
||||
expect(lastFrame()).equals(`This is a
|
||||
long line
|
||||
of text`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles mixed wrapping and non-wrapping segments', () => {
|
||||
const multilineText = `This part will wrap around.
|
||||
And has a line break.
|
||||
Leading spaces preserved.`;
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={20} maxHeight={20}>
|
||||
<Box>
|
||||
@@ -125,10 +129,11 @@ Longer No Wrap: This
|
||||
arou
|
||||
nd.`,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles words longer than maxWidth by splitting them', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
@@ -143,10 +148,11 @@ istic
|
||||
expia
|
||||
lidoc
|
||||
ious`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not truncate when maxHeight is undefined', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
|
||||
<Box>
|
||||
@@ -160,10 +166,11 @@ ious`);
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
Line 2`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -180,10 +187,11 @@ Line 2`);
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
@@ -200,10 +208,11 @@ Line 3`);
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders an empty box for empty children', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
@@ -211,10 +220,11 @@ Line 3`);
|
||||
// Expect an empty string or a box with nothing in it.
|
||||
// Ink renders an empty box as an empty string.
|
||||
expect(lastFrame()).equals('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text with multi-byte unicode characters correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
@@ -228,10 +238,11 @@ Line 3`);
|
||||
// With maxWidth=5, it should wrap after the second character.
|
||||
expect(lastFrame()).equals(`你好
|
||||
世界`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text with multi-byte emoji characters correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
@@ -246,10 +257,11 @@ Line 3`);
|
||||
expect(lastFrame()).equals(`🐶🐶
|
||||
🐶🐶
|
||||
🐶`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('falls back to an ellipsis when width is extremely small', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={2} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -261,10 +273,11 @@ Line 3`);
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals('N…');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates long non-wrapping text with ellipsis', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -276,10 +289,11 @@ Line 3`);
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals('AB…');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates non-wrapping text containing line breaks', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -291,10 +305,11 @@ Line 3`);
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`A\n…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates emoji characters correctly with ellipsis', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
@@ -306,10 +321,11 @@ Line 3`);
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`🐶…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows ellipsis for multiple rows with long non-wrapping text', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={3}>
|
||||
<Box>
|
||||
@@ -329,10 +345,11 @@ Line 3`);
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`AA…\nBB…\nCC…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('accounts for additionalHiddenLinesCount', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
||||
<Box>
|
||||
@@ -350,10 +367,11 @@ Line 3`);
|
||||
// 1 line is hidden by overflow, 5 are additionally hidden.
|
||||
expect(lastFrame()).equals(`... first 7 lines hidden ...
|
||||
Line 3`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles React.Fragment as a child', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<>
|
||||
@@ -373,6 +391,7 @@ Line 3`);
|
||||
expect(lastFrame()).equals(`Line 1 from Fragment
|
||||
Line 2 from Fragment
|
||||
Line 3 direct child`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clips a long single text child from the top', () => {
|
||||
@@ -381,7 +400,7 @@ Line 3 direct child`);
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
@@ -397,6 +416,7 @@ Line 3 direct child`);
|
||||
].join('\n');
|
||||
|
||||
expect(lastFrame()).equals(expected);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clips a long single text child from the bottom', () => {
|
||||
@@ -405,7 +425,7 @@ Line 3 direct child`);
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
|
||||
<Box>
|
||||
@@ -421,5 +441,6 @@ Line 3 direct child`);
|
||||
].join('\n');
|
||||
|
||||
expect(lastFrame()).equals(expected);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { TextInput } from './TextInput.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChatList } from './ChatList.js';
|
||||
import type { ChatDetail } from '../../types.js';
|
||||
@@ -22,14 +22,16 @@ const mockChats: ChatDetail[] = [
|
||||
|
||||
describe('<ChatList />', () => {
|
||||
it('renders correctly with a list of chats', () => {
|
||||
const { lastFrame } = render(<ChatList chats={mockChats} />);
|
||||
const { lastFrame, unmount } = render(<ChatList chats={mockChats} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with no chats', () => {
|
||||
const { lastFrame } = render(<ChatList chats={[]} />);
|
||||
const { lastFrame, unmount } = render(<ChatList chats={[]} />);
|
||||
expect(lastFrame()).toContain('No saved conversation checkpoints found.');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles invalid date formats gracefully', () => {
|
||||
@@ -39,8 +41,11 @@ describe('<ChatList />', () => {
|
||||
mtime: 'an-invalid-date-string',
|
||||
},
|
||||
];
|
||||
const { lastFrame } = render(<ChatList chats={mockChatsWithInvalidDate} />);
|
||||
const { lastFrame, unmount } = render(
|
||||
<ChatList chats={mockChatsWithInvalidDate} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('(Invalid Date)');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { ExtensionUpdateState } from '../../state/extensions.js';
|
||||
@@ -57,27 +57,30 @@ describe('<ExtensionsList />', () => {
|
||||
|
||||
it('should render "No extensions installed." if there are no extensions', () => {
|
||||
mockUIState(new Map());
|
||||
const { lastFrame } = render(<ExtensionsList extensions={[]} />);
|
||||
const { lastFrame, unmount } = render(<ExtensionsList extensions={[]} />);
|
||||
expect(lastFrame()).toContain('No extensions installed.');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render a list of extensions with their version and status', () => {
|
||||
mockUIState(new Map());
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<ExtensionsList extensions={mockExtensions} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ext-one (v1.0.0) - active');
|
||||
expect(output).toContain('ext-two (v2.1.0) - active');
|
||||
expect(output).toContain('ext-disabled (v3.0.0) - disabled');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display "unknown state" if an extension has no update state', () => {
|
||||
mockUIState(new Map());
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<ExtensionsList extensions={[mockExtensions[0]]} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('(unknown state)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
const stateTestCases = [
|
||||
@@ -115,10 +118,11 @@ describe('<ExtensionsList />', () => {
|
||||
it(`should correctly display the state: ${state}`, () => {
|
||||
const updateState = new Map([[mockExtensions[0].name, state]]);
|
||||
mockUIState(updateState);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<ExtensionsList extensions={[mockExtensions[0]]} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(expectedText);
|
||||
unmount();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { McpStatus } from './McpStatus.js';
|
||||
import { MCPServerStatus } from '@google/gemini-cli-core';
|
||||
@@ -46,32 +46,36 @@ describe('McpStatus', () => {
|
||||
};
|
||||
|
||||
it('renders correctly with a connected server', () => {
|
||||
const { lastFrame } = render(<McpStatus {...baseProps} />);
|
||||
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with authenticated OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus {...baseProps} authStatus={{ 'server-1': 'authenticated' }} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with expired OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus {...baseProps} authStatus={{ 'server-1': 'expired' }} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with unauthenticated OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
authStatus={{ 'server-1': 'unauthenticated' }}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with a disconnected server', async () => {
|
||||
@@ -79,26 +83,29 @@ describe('McpStatus', () => {
|
||||
await import('@google/gemini-cli-core'),
|
||||
'getMCPServerStatus',
|
||||
).mockReturnValue(MCPServerStatus.DISCONNECTED);
|
||||
const { lastFrame } = render(<McpStatus {...baseProps} />);
|
||||
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly when discovery is in progress', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus {...baseProps} discoveryInProgress={true} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with schema enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus {...baseProps} showSchema={true} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with parametersJsonSchema', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
tools={[
|
||||
@@ -120,10 +127,11 @@ describe('McpStatus', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with prompts', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
prompts={[
|
||||
@@ -136,22 +144,25 @@ describe('McpStatus', () => {
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with a blocked server', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
blockedServers={[{ name: 'server-1', extensionName: 'test-extension' }]}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with a connecting server', () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, unmount } = render(
|
||||
<McpStatus {...baseProps} connectingServers={['server-1']} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi } from 'vitest';
|
||||
import type { Key } from './KeypressContext.js';
|
||||
@@ -275,7 +276,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
act(() => writeSequence(pastedText));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -1020,7 +1021,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
}
|
||||
|
||||
// Should parse once complete
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'escape',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { type MutableRefObject, Component, type ReactNode } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
|
||||
import { act } from 'react';
|
||||
import type { SessionMetrics } from './SessionContext.js';
|
||||
@@ -58,7 +58,7 @@ describe('SessionStatsContext', () => {
|
||||
ReturnType<typeof useSessionStats> | undefined
|
||||
> = { current: undefined };
|
||||
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<SessionStatsProvider>
|
||||
<TestHarness contextRef={contextRef} />
|
||||
</SessionStatsProvider>,
|
||||
@@ -69,6 +69,7 @@ describe('SessionStatsContext', () => {
|
||||
expect(stats?.sessionStartTime).toBeInstanceOf(Date);
|
||||
expect(stats?.metrics).toBeDefined();
|
||||
expect(stats?.metrics.models).toEqual({});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should update metrics when the uiTelemetryService emits an update', () => {
|
||||
@@ -76,7 +77,7 @@ describe('SessionStatsContext', () => {
|
||||
ReturnType<typeof useSessionStats> | undefined
|
||||
> = { current: undefined };
|
||||
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<SessionStatsProvider>
|
||||
<TestHarness contextRef={contextRef} />
|
||||
</SessionStatsProvider>,
|
||||
@@ -142,6 +143,7 @@ describe('SessionStatsContext', () => {
|
||||
const stats = contextRef.current?.stats;
|
||||
expect(stats?.metrics).toEqual(newMetrics);
|
||||
expect(stats?.lastPromptTokenCount).toBe(100);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not update metrics if the data is the same', () => {
|
||||
@@ -156,7 +158,7 @@ describe('SessionStatsContext', () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<SessionStatsProvider>
|
||||
<CountingTestHarness />
|
||||
</SessionStatsProvider>,
|
||||
@@ -228,6 +230,7 @@ describe('SessionStatsContext', () => {
|
||||
});
|
||||
|
||||
expect(renderCount).toBe(3);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should throw an error when useSessionStats is used outside of a provider', () => {
|
||||
@@ -235,7 +238,7 @@ describe('SessionStatsContext', () => {
|
||||
// Suppress console.error from React for this test
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
const { unmount } = render(
|
||||
<ErrorBoundary onError={onError}>
|
||||
<TestHarness contextRef={{ current: undefined }} />
|
||||
</ErrorBoundary>,
|
||||
@@ -248,5 +251,6 @@ describe('SessionStatsContext', () => {
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import {
|
||||
vi,
|
||||
describe,
|
||||
@@ -18,13 +19,30 @@ import {
|
||||
|
||||
const mockIsBinary = vi.hoisted(() => vi.fn());
|
||||
const mockShellExecutionService = vi.hoisted(() => vi.fn());
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
ShellExecutionService: { execute: mockShellExecutionService },
|
||||
isBinary: mockIsBinary,
|
||||
}));
|
||||
vi.mock('fs');
|
||||
vi.mock('os');
|
||||
vi.mock('crypto');
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
ShellExecutionService: { execute: mockShellExecutionService },
|
||||
isBinary: mockIsBinary,
|
||||
};
|
||||
});
|
||||
vi.mock('node:fs');
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
const mocked = {
|
||||
...actual,
|
||||
homedir: vi.fn(() => '/home/user'),
|
||||
platform: vi.fn(() => 'linux'),
|
||||
tmpdir: vi.fn(() => '/tmp'),
|
||||
};
|
||||
return {
|
||||
...mocked,
|
||||
default: mocked,
|
||||
};
|
||||
});
|
||||
vi.mock('node:crypto');
|
||||
vi.mock('../utils/textUtils.js');
|
||||
|
||||
import {
|
||||
@@ -134,7 +152,7 @@ describe('useShellCommandProcessor', () => {
|
||||
it('should initiate command execution and set pending state', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('ls -l', new AbortController().signal);
|
||||
});
|
||||
|
||||
@@ -233,7 +251,7 @@ describe('useShellCommandProcessor', () => {
|
||||
|
||||
it('should update UI for text streams (non-interactive)', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand(
|
||||
'stream',
|
||||
new AbortController().signal,
|
||||
@@ -256,7 +274,7 @@ describe('useShellCommandProcessor', () => {
|
||||
|
||||
// Wait for the async PID update to happen.
|
||||
// Call 1: Initial, Call 2: PID update
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(setPendingHistoryItemMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -365,7 +383,7 @@ describe('useShellCommandProcessor', () => {
|
||||
vi.mocked(os.platform).mockReturnValue('win32');
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('dir', new AbortController().signal);
|
||||
});
|
||||
|
||||
@@ -377,6 +395,11 @@ describe('useShellCommandProcessor', () => {
|
||||
false,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
resolveExecutionPromise(createMockServiceResult());
|
||||
});
|
||||
await act(async () => await onExecMock.mock.calls[0][0]);
|
||||
});
|
||||
|
||||
it('should handle command abort and display cancelled status', async () => {
|
||||
@@ -559,23 +582,21 @@ describe('useShellCommandProcessor', () => {
|
||||
it('should set activeShellPtyId when a command with a PID starts', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('ls', new AbortController().signal);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
});
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
});
|
||||
|
||||
it('should update the pending history item with the ptyId', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('ls', new AbortController().signal);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Wait for the second call which is the functional update
|
||||
expect(setPendingHistoryItemMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -594,16 +615,14 @@ describe('useShellCommandProcessor', () => {
|
||||
it('should reset activeShellPtyId to null after successful execution', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('ls', new AbortController().signal);
|
||||
});
|
||||
const execPromise = onExecMock.mock.calls[0][0];
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
});
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
resolveExecutionPromise(createMockServiceResult());
|
||||
});
|
||||
await act(async () => await execPromise);
|
||||
@@ -614,7 +633,7 @@ describe('useShellCommandProcessor', () => {
|
||||
it('should reset activeShellPtyId to null after failed execution', async () => {
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand(
|
||||
'bad-cmd',
|
||||
new AbortController().signal,
|
||||
@@ -622,11 +641,9 @@ describe('useShellCommandProcessor', () => {
|
||||
});
|
||||
const execPromise = onExecMock.mock.calls[0][0];
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
});
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
resolveExecutionPromise(createMockServiceResult({ exitCode: 1 }));
|
||||
});
|
||||
await act(async () => await execPromise);
|
||||
@@ -638,7 +655,7 @@ describe('useShellCommandProcessor', () => {
|
||||
let rejectResultPromise: (reason?: unknown) => void;
|
||||
mockShellExecutionService.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
pid: 1234_5,
|
||||
pid: 12345,
|
||||
result: new Promise((_, reject) => {
|
||||
rejectResultPromise = reject;
|
||||
}),
|
||||
@@ -646,16 +663,14 @@ describe('useShellCommandProcessor', () => {
|
||||
);
|
||||
const { result } = renderProcessorHook();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('cmd', new AbortController().signal);
|
||||
});
|
||||
const execPromise = onExecMock.mock.calls[0][0];
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
});
|
||||
expect(result.current.activeShellPtyId).toBe(12345);
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
rejectResultPromise(new Error('Failure'));
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
|
||||
import type {
|
||||
CommandContext,
|
||||
@@ -15,7 +16,7 @@ import type {
|
||||
} from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { MessageType, type SlashCommandProcessorResult } from '../types.js';
|
||||
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
||||
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
||||
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
|
||||
@@ -38,6 +39,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...original,
|
||||
logSlashCommand,
|
||||
getIdeInstaller: vi.fn().mockReturnValue(null),
|
||||
IdeClient: {
|
||||
getInstance: vi.fn().mockResolvedValue({
|
||||
addStatusChangeListener: vi.fn(),
|
||||
removeStatusChangeListener: vi.fn(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -114,6 +121,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
const mockConfig = makeFakeConfig({});
|
||||
const mockSettings = {} as LoadedSettings;
|
||||
|
||||
let unmountHook: (() => Promise<void>) | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(BuiltinCommandLoader).mockClear();
|
||||
@@ -122,7 +131,14 @@ describe('useSlashCommandProcessor', () => {
|
||||
mockMcpLoadCommands.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
const setupProcessorHook = (
|
||||
afterEach(async () => {
|
||||
if (unmountHook) {
|
||||
await unmountHook();
|
||||
unmountHook = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const setupProcessorHook = async (
|
||||
builtinCommands: SlashCommand[] = [],
|
||||
fileCommands: SlashCommand[] = [],
|
||||
mcpCommands: SlashCommand[] = [],
|
||||
@@ -132,54 +148,66 @@ describe('useSlashCommandProcessor', () => {
|
||||
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
|
||||
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
|
||||
|
||||
let hookResult: ReturnType<typeof useSlashCommandProcessor>;
|
||||
let result!: { current: ReturnType<typeof useSlashCommandProcessor> };
|
||||
let unmount!: () => void;
|
||||
let rerender!: (props?: unknown) => void;
|
||||
|
||||
function TestComponent() {
|
||||
hookResult = useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockAddItem,
|
||||
mockClearItems,
|
||||
mockLoadHistory,
|
||||
vi.fn(), // refreshStatic
|
||||
vi.fn(), // toggleVimEnabled
|
||||
setIsProcessing,
|
||||
vi.fn(), // setGeminiMdFileCount
|
||||
{
|
||||
openAuthDialog: mockOpenAuthDialog,
|
||||
openThemeDialog: mockOpenThemeDialog,
|
||||
openEditorDialog: vi.fn(),
|
||||
openPrivacyNotice: vi.fn(),
|
||||
openSettingsDialog: vi.fn(),
|
||||
openModelDialog: mockOpenModelDialog,
|
||||
openPermissionsDialog: vi.fn(),
|
||||
quit: mockSetQuittingMessages,
|
||||
setDebugMessage: vi.fn(),
|
||||
toggleCorgiMode: vi.fn(),
|
||||
toggleDebugProfiler: vi.fn(),
|
||||
dispatchExtensionStateUpdate: vi.fn(),
|
||||
addConfirmUpdateExtensionRequest: vi.fn(),
|
||||
},
|
||||
new Map(), // extensionsUpdateState
|
||||
true, // isConfigInitialized
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockAddItem,
|
||||
mockClearItems,
|
||||
mockLoadHistory,
|
||||
vi.fn(), // refreshStatic
|
||||
vi.fn(), // toggleVimEnabled
|
||||
setIsProcessing,
|
||||
vi.fn(), // setGeminiMdFileCount
|
||||
{
|
||||
openAuthDialog: mockOpenAuthDialog,
|
||||
openThemeDialog: mockOpenThemeDialog,
|
||||
openEditorDialog: vi.fn(),
|
||||
openPrivacyNotice: vi.fn(),
|
||||
openSettingsDialog: vi.fn(),
|
||||
openModelDialog: mockOpenModelDialog,
|
||||
openPermissionsDialog: vi.fn(),
|
||||
quit: mockSetQuittingMessages,
|
||||
setDebugMessage: vi.fn(),
|
||||
toggleCorgiMode: vi.fn(),
|
||||
toggleDebugProfiler: vi.fn(),
|
||||
dispatchExtensionStateUpdate: vi.fn(),
|
||||
addConfirmUpdateExtensionRequest: vi.fn(),
|
||||
},
|
||||
new Map(), // extensionsUpdateState
|
||||
true, // isConfigInitialized
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
rerender = hook.rerender;
|
||||
});
|
||||
|
||||
const { unmount, rerender } = render(<TestComponent />);
|
||||
unmountHook = async () => unmount();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.slashCommands).toBeDefined();
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return hookResult;
|
||||
return result.current;
|
||||
},
|
||||
unmount,
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
rerender: async () => {
|
||||
rerender();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('Initialization and Command Loading', () => {
|
||||
it('should initialize CommandService with all required loaders', () => {
|
||||
setupProcessorHook();
|
||||
it('should initialize CommandService with all required loaders', async () => {
|
||||
await setupProcessorHook();
|
||||
expect(BuiltinCommandLoader).toHaveBeenCalledWith(mockConfig);
|
||||
expect(FileCommandLoader).toHaveBeenCalledWith(mockConfig);
|
||||
expect(McpPromptLoader).toHaveBeenCalledWith(mockConfig);
|
||||
@@ -187,9 +215,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should call loadCommands and populate state after mounting', async () => {
|
||||
const testCommand = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([testCommand]);
|
||||
const result = await setupProcessorHook([testCommand]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -201,9 +229,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should provide an immutable array of commands to consumers', async () => {
|
||||
const testCommand = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([testCommand]);
|
||||
const result = await setupProcessorHook([testCommand]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -229,9 +257,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
CommandKind.FILE,
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([builtinCommand], [fileCommand]);
|
||||
const result = await setupProcessorHook([builtinCommand], [fileCommand]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// The service should only return one command with the name 'override'
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
@@ -248,10 +276,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
describe('Command Execution Logic', () => {
|
||||
it('should display an error for an unknown command', async () => {
|
||||
const result = setupProcessorHook();
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toBeDefined(),
|
||||
);
|
||||
const result = await setupProcessorHook();
|
||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/nonexistent');
|
||||
@@ -281,10 +307,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = setupProcessorHook([parentCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([parentCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/parent');
|
||||
@@ -317,10 +341,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = setupProcessorHook([parentCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([parentCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/parent child with args');
|
||||
@@ -343,7 +365,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('sets isProcessing to false if the the input is not a command', async () => {
|
||||
const setMockIsProcessing = vi.fn();
|
||||
const result = setupProcessorHook([], [], [], setMockIsProcessing);
|
||||
const result = await setupProcessorHook([], [], [], setMockIsProcessing);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('imnotacommand');
|
||||
@@ -359,16 +381,14 @@ describe('useSlashCommandProcessor', () => {
|
||||
action: vi.fn().mockRejectedValue(new Error('oh no!')),
|
||||
});
|
||||
|
||||
const result = setupProcessorHook(
|
||||
const result = await setupProcessorHook(
|
||||
[failCommand],
|
||||
[],
|
||||
[],
|
||||
setMockIsProcessing,
|
||||
);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toBeDefined(),
|
||||
);
|
||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/fail');
|
||||
@@ -385,10 +405,13 @@ describe('useSlashCommandProcessor', () => {
|
||||
action: () => new Promise((resolve) => setTimeout(resolve, 50)),
|
||||
});
|
||||
|
||||
const result = setupProcessorHook([command], [], [], mockSetIsProcessing);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
const result = await setupProcessorHook(
|
||||
[command],
|
||||
[],
|
||||
[],
|
||||
mockSetIsProcessing,
|
||||
);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
const executionPromise = act(async () => {
|
||||
await result.current.handleSlashCommand('/long-running');
|
||||
@@ -413,10 +436,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
name: 'themecmd',
|
||||
action: vi.fn().mockResolvedValue({ type: 'dialog', dialog: 'theme' }),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/themecmd');
|
||||
@@ -430,10 +451,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
name: 'modelcmd',
|
||||
action: vi.fn().mockResolvedValue({ type: 'dialog', dialog: 'model' }),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/modelcmd');
|
||||
@@ -457,10 +476,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
clientHistory: [{ role: 'user', parts: [{ text: 'old prompt' }] }],
|
||||
}),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/load');
|
||||
@@ -495,10 +512,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/loadwiththoughts');
|
||||
@@ -516,11 +531,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
name: 'exit',
|
||||
action: quitAction,
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
const result = await setupProcessorHook([command]);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit');
|
||||
@@ -541,10 +554,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
CommandKind.FILE,
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([], [fileCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([], [fileCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
let actionResult;
|
||||
await act(async () => {
|
||||
@@ -575,10 +586,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
CommandKind.MCP_PROMPT,
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([], [], [mcpCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([], [], [mcpCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
let actionResult;
|
||||
await act(async () => {
|
||||
@@ -619,20 +628,19 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should set confirmation request when action returns confirm_shell_commands', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
// This is intentionally not awaited, because the promise it returns
|
||||
// will not resolve until the user responds to the confirmation.
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
// Trigger command, don't await it yet as it suspends for confirmation
|
||||
await act(async () => {
|
||||
void result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
|
||||
// We now wait for the state to be updated with the request.
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.shellConfirmationRequest?.commands).toEqual([
|
||||
@@ -641,18 +649,18 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should do nothing if user cancels confirmation', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
await act(async () => {
|
||||
void result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
|
||||
// Wait for the confirmation dialog to be set
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const onConfirm = result.current.shellConfirmationRequest?.onConfirm;
|
||||
@@ -676,16 +684,19 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should re-run command with one-time allowlist on "Proceed Once"', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
let commandPromise:
|
||||
| Promise<false | SlashCommandProcessorResult>
|
||||
| undefined;
|
||||
await act(async () => {
|
||||
commandPromise = result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const onConfirm = result.current.shellConfirmationRequest?.onConfirm;
|
||||
@@ -702,10 +713,14 @@ describe('useSlashCommandProcessor', () => {
|
||||
onConfirm!(ToolConfirmationOutcome.ProceedOnce, ['rm -rf /']);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await commandPromise;
|
||||
});
|
||||
|
||||
expect(result.current.shellConfirmationRequest).toBeNull();
|
||||
|
||||
// The action should have been called twice (initial + re-run).
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockCommandAction).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -725,23 +740,26 @@ describe('useSlashCommandProcessor', () => {
|
||||
// Verify the session-wide allowlist was NOT permanently updated.
|
||||
// Re-render the hook by calling a no-op command to get the latest context.
|
||||
await act(async () => {
|
||||
result.current.handleSlashCommand('/no-op');
|
||||
await result.current.handleSlashCommand('/no-op');
|
||||
});
|
||||
const finalContext = result.current.commandContext;
|
||||
expect(finalContext.session.sessionShellAllowlist.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should re-run command and update session allowlist on "Proceed Always"', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
let commandPromise:
|
||||
| Promise<false | SlashCommandProcessorResult>
|
||||
| undefined;
|
||||
await act(async () => {
|
||||
commandPromise = result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const onConfirm = result.current.shellConfirmationRequest?.onConfirm;
|
||||
@@ -755,8 +773,12 @@ describe('useSlashCommandProcessor', () => {
|
||||
onConfirm!(ToolConfirmationOutcome.ProceedAlways, ['rm -rf /']);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await commandPromise;
|
||||
});
|
||||
|
||||
expect(result.current.shellConfirmationRequest).toBeNull();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockCommandAction).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -766,7 +788,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
);
|
||||
|
||||
// Check that the session-wide allowlist WAS updated.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const finalContext = result.current.commandContext;
|
||||
expect(finalContext.session.sessionShellAllowlist.has('rm -rf /')).toBe(
|
||||
true,
|
||||
@@ -778,10 +800,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
describe('Command Parsing and Matching', () => {
|
||||
it('should be case-sensitive', async () => {
|
||||
const command = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
// Use uppercase when command is lowercase
|
||||
@@ -806,10 +826,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
description: 'a command with an alias',
|
||||
action,
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/alias');
|
||||
@@ -824,10 +842,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
it('should handle extra whitespace around the command', async () => {
|
||||
const action = vi.fn();
|
||||
const command = createTestCommand({ name: 'test', action });
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand(' /test with-args ');
|
||||
@@ -839,10 +855,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
it('should handle `?` as a command prefix', async () => {
|
||||
const action = vi.fn();
|
||||
const command = createTestCommand({ name: 'help', action });
|
||||
const result = setupProcessorHook([command]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
const result = await setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('?help');
|
||||
@@ -870,9 +884,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
CommandKind.FILE,
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([], [fileCommand], [mcpCommand]);
|
||||
const result = await setupProcessorHook([], [fileCommand], [mcpCommand]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// The service should only return one command with the name 'override'
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
@@ -906,9 +920,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
// The order of commands in the final loaded array is not guaranteed,
|
||||
// so the test must work regardless of which comes first.
|
||||
const result = setupProcessorHook([quitCommand], [exitCommand]);
|
||||
const result = await setupProcessorHook([quitCommand], [exitCommand]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -933,10 +947,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
CommandKind.FILE,
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([quitCommand], [exitCommand]);
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(2),
|
||||
);
|
||||
const result = await setupProcessorHook([quitCommand], [exitCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit');
|
||||
@@ -951,9 +963,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
describe('Lifecycle', () => {
|
||||
it('should abort command loading when the hook unmounts', () => {
|
||||
it('should abort command loading when the hook unmounts', async () => {
|
||||
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
|
||||
const { unmount } = setupProcessorHook();
|
||||
const { unmount } = await setupProcessorHook();
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -996,8 +1008,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should log a simple slash command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1015,8 +1027,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('logs nothing for a bogus command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1027,8 +1039,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('logs a failure event for a failed command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1046,8 +1058,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should log a slash command with a subcommand', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1064,8 +1076,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should log the command path when an alias is used', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1080,8 +1092,8 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
it('should not log for unknown commands', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await vi.waitFor(() =>
|
||||
const result = await setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { act, useState } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useAtCompletion } from './useAtCompletion.js';
|
||||
import type { Config, FileSearch } from '@google/gemini-cli-core';
|
||||
import { FileSearchFactory } from '@google/gemini-cli-core';
|
||||
@@ -74,10 +75,11 @@ describe('useAtCompletion', () => {
|
||||
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'src/',
|
||||
'src/components/',
|
||||
@@ -104,7 +106,7 @@ describe('useAtCompletion', () => {
|
||||
useTestHarnessForAtCompletion(true, 'src/', mockConfig, testRootDir),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -127,7 +129,7 @@ describe('useAtCompletion', () => {
|
||||
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -164,7 +166,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// The hook should find 'cRaZycAsE.txt' even though the pattern is 'CrAzYCaSe'.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'cRaZycAsE.txt',
|
||||
]);
|
||||
@@ -192,12 +194,12 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// It's initially true because the effect runs synchronously.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoadingSuggestions).toBe(true);
|
||||
});
|
||||
|
||||
// Wait for the loading to complete.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoadingSuggestions).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -212,7 +214,7 @@ describe('useAtCompletion', () => {
|
||||
{ initialProps: { pattern: 'a' } },
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'a.txt',
|
||||
]);
|
||||
@@ -222,7 +224,7 @@ describe('useAtCompletion', () => {
|
||||
rerender({ pattern: 'b' });
|
||||
|
||||
// Wait for the final result
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'b.txt',
|
||||
]);
|
||||
@@ -265,7 +267,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// Wait for the initial search to complete (using real timers)
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'a.txt',
|
||||
]);
|
||||
@@ -295,7 +297,7 @@ describe('useAtCompletion', () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
// Wait for the search results to be processed
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'b.txt',
|
||||
]);
|
||||
@@ -326,7 +328,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// Wait for the hook to be ready (initialization is complete)
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockFileSearch.search).toHaveBeenCalledWith(
|
||||
'a',
|
||||
expect.any(Object),
|
||||
@@ -342,7 +344,7 @@ describe('useAtCompletion', () => {
|
||||
expect(abortSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Wait for the final result, which should be from the second, faster search.
|
||||
await vi.waitFor(
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual(['b']);
|
||||
},
|
||||
@@ -369,7 +371,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// Wait for the hook to be ready and have suggestions
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'a.txt',
|
||||
]);
|
||||
@@ -401,7 +403,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// Wait for the hook to enter the error state
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoadingSuggestions).toBe(false);
|
||||
});
|
||||
expect(result.current.suggestions).toEqual([]); // No suggestions on error
|
||||
@@ -432,7 +434,7 @@ describe('useAtCompletion', () => {
|
||||
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -453,7 +455,7 @@ describe('useAtCompletion', () => {
|
||||
useTestHarnessForAtCompletion(true, '', undefined, testRootDir),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -481,7 +483,7 @@ describe('useAtCompletion', () => {
|
||||
);
|
||||
|
||||
// Wait for initial suggestions from the first directory
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'file1.txt',
|
||||
]);
|
||||
@@ -493,13 +495,13 @@ describe('useAtCompletion', () => {
|
||||
});
|
||||
|
||||
// After CWD changes, suggestions should be cleared and it should load again.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoadingSuggestions).toBe(true);
|
||||
expect(result.current.suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
// Wait for the new suggestions from the second directory
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'file2.txt',
|
||||
]);
|
||||
@@ -537,7 +539,7 @@ describe('useAtCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { act, useEffect } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useCommandCompletion } from './useCommandCompletion.js';
|
||||
import type { CommandContext } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
@@ -170,7 +171,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('@file');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -184,7 +185,7 @@ describe('useCommandCompletion', () => {
|
||||
);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.showSuggestions).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -210,7 +211,7 @@ describe('useCommandCompletion', () => {
|
||||
const text = '@src/a\\ file.txt';
|
||||
renderCommandCompletionHook(text);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(useAtCompletion).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
@@ -226,7 +227,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
renderCommandCompletionHook(text, cursorOffset);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(useAtCompletion).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
@@ -268,7 +269,7 @@ describe('useCommandCompletion', () => {
|
||||
shellModeActive,
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(expectedSuggestions);
|
||||
expect(result.current.showSuggestions).toBe(
|
||||
expectedShowSuggestions,
|
||||
@@ -317,7 +318,7 @@ describe('useCommandCompletion', () => {
|
||||
it('should navigate up through suggestions with wrap-around', async () => {
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -333,7 +334,7 @@ describe('useCommandCompletion', () => {
|
||||
it('should navigate down through suggestions with wrap-around', async () => {
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -352,7 +353,7 @@ describe('useCommandCompletion', () => {
|
||||
it('should handle navigation with multiple suggestions', async () => {
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -379,7 +380,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(
|
||||
mockSuggestions.length,
|
||||
);
|
||||
@@ -398,7 +399,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('/mem');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -416,7 +417,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('@src/fi');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -437,7 +438,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook(text, cursorOffset);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -457,7 +458,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('@src/comp');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -477,7 +478,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
const { result } = renderCommandCompletionHook('@src\\comp');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { act, useCallback } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useConsoleMessages } from './useConsoleMessages.js';
|
||||
|
||||
describe('useConsoleMessages', () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type MockedFunction,
|
||||
} from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useEditorSettings } from './useEditorSettings.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
@@ -11,7 +11,8 @@ import * as path from 'node:path';
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
import { useExtensionUpdates } from './useExtensionUpdates.js';
|
||||
import { GEMINI_DIR } from '@google/gemini-cli-core';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
@@ -102,7 +103,7 @@ describe('useExtensionUpdates', () => {
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
@@ -152,7 +153,7 @@ describe('useExtensionUpdates', () => {
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -230,7 +231,7 @@ describe('useExtensionUpdates', () => {
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(2);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
@@ -313,7 +314,7 @@ describe('useExtensionUpdates', () => {
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { useFocus } from './useFocus.js';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { vi, type Mock, type MockInstance } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useFolderTrust } from './useFolderTrust.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
|
||||
@@ -88,7 +89,7 @@ describe('useFolderTrust', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFolderTrust(mockSettings, onTrustChange, addItem),
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isFolderTrustDialogOpen).toBe(true);
|
||||
});
|
||||
expect(onTrustChange).toHaveBeenCalledWith(undefined);
|
||||
@@ -129,7 +130,7 @@ describe('useFolderTrust', () => {
|
||||
useFolderTrust(mockSettings, onTrustChange, addItem),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isTrusted).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -139,7 +140,7 @@ describe('useFolderTrust', () => {
|
||||
);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
|
||||
'/test/path',
|
||||
TrustLevel.TRUST_FOLDER,
|
||||
@@ -207,7 +208,7 @@ describe('useFolderTrust', () => {
|
||||
);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockTrustedFolders.setValue).not.toHaveBeenCalled();
|
||||
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||
expect(result.current.isFolderTrustDialogOpen).toBe(true);
|
||||
@@ -229,7 +230,7 @@ describe('useFolderTrust', () => {
|
||||
useFolderTrust(mockSettings, onTrustChange, addItem),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isTrusted).toBe(false);
|
||||
});
|
||||
|
||||
@@ -237,7 +238,7 @@ describe('useFolderTrust', () => {
|
||||
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isRestarting).toBe(true);
|
||||
expect(result.current.isFolderTrustDialogOpen).toBe(true); // Dialog should stay open
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Mock, MockInstance } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useGeminiStream } from './useGeminiStream.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import * as atCommandProcessor from './atCommandProcessor.js';
|
||||
@@ -507,7 +508,7 @@ describe('useGeminiStream', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -590,7 +591,7 @@ describe('useGeminiStream', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
|
||||
expect(client.addHistory).toHaveBeenCalledWith({
|
||||
role: 'user',
|
||||
@@ -702,7 +703,7 @@ describe('useGeminiStream', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// The tools should be marked as submitted locally
|
||||
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith([
|
||||
'cancel-1',
|
||||
@@ -840,7 +841,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// 5. Wait for submitQuery to be called
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
toolCallResponseParts,
|
||||
expect.any(AbortSignal),
|
||||
@@ -889,7 +890,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Wait for the first part of the response
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamingState).toBe(StreamingState.Responding);
|
||||
});
|
||||
|
||||
@@ -897,7 +898,7 @@ describe('useGeminiStream', () => {
|
||||
simulateEscapeKeyPress();
|
||||
|
||||
// Verify cancellation message is added
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
@@ -1030,7 +1031,7 @@ describe('useGeminiStream', () => {
|
||||
result.current.submitQuery('long running query');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamingState).toBe(StreamingState.Responding);
|
||||
});
|
||||
|
||||
@@ -1038,13 +1039,12 @@ describe('useGeminiStream', () => {
|
||||
simulateEscapeKeyPress();
|
||||
|
||||
// Allow the stream to continue
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
continueStream();
|
||||
// Wait a bit to see if the second part is processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
// Wait a bit to see if the second part is processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// The text should not have been updated with " Canceled"
|
||||
const lastCall = mockAddItem.mock.calls.find(
|
||||
(call) => call[0].type === 'gemini',
|
||||
@@ -1138,7 +1138,7 @@ describe('useGeminiStream', () => {
|
||||
expect(mockCancelAllToolCalls).toHaveBeenCalled();
|
||||
|
||||
// A cancellation message should be added to history
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: 'Request cancelled.',
|
||||
@@ -1167,7 +1167,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/memory add "test fact"');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockScheduleToolCalls).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
@@ -1194,7 +1194,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/help');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/help');
|
||||
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
|
||||
expect(mockSendMessageStream).not.toHaveBeenCalled(); // No LLM call made
|
||||
@@ -1215,7 +1215,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/my-custom-command');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
|
||||
'/my-custom-command',
|
||||
);
|
||||
@@ -1250,7 +1250,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/emptycmd');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/emptycmd');
|
||||
expect(localMockSendMessageStream).toHaveBeenCalledWith(
|
||||
'',
|
||||
@@ -1268,7 +1268,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('// This is a line comment');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
expect(localMockSendMessageStream).toHaveBeenCalledWith(
|
||||
'// This is a line comment',
|
||||
@@ -1286,7 +1286,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/* This is a block comment */');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
expect(localMockSendMessageStream).toHaveBeenCalledWith(
|
||||
'/* This is a block comment */',
|
||||
@@ -1324,7 +1324,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('/about');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1401,7 +1401,7 @@ describe('useGeminiStream', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockPerformMemoryRefresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1457,7 +1457,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// 3. Assertion
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockParseAndFormatApiError).toHaveBeenCalledWith(
|
||||
'Rate limit exceeded',
|
||||
mockAuthType,
|
||||
@@ -1990,7 +1990,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Check that the info message was added
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
@@ -2050,7 +2050,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Check that the message was added without suggestion
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
@@ -2105,7 +2105,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Check that the message was added with suggestion
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
@@ -2161,7 +2161,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Check that onCancelSubmit was called
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(onCancelSubmitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2360,7 +2360,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery(`Test ${reason}`);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
@@ -2487,7 +2487,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Wait for the first response to complete
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
@@ -2520,7 +2520,7 @@ describe('useGeminiStream', () => {
|
||||
// We can verify this by checking that the LoadingIndicator would not show the previous thought
|
||||
// The actual thought state is internal to the hook, but we can verify the behavior
|
||||
// by ensuring the second response doesn't show the previous thought
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
@@ -2638,7 +2638,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Verify cancellation message was added
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
@@ -2696,7 +2696,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Verify error message was added
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
@@ -2747,7 +2747,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
expect(
|
||||
typeof result.current.loopDetectionConfirmationRequest?.onComplete,
|
||||
@@ -2795,7 +2795,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Wait for confirmation request to be set
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -2824,7 +2824,7 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
|
||||
// Verify that the request was retried
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
||||
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
@@ -2860,7 +2860,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Wait for confirmation request to be set
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -2907,7 +2907,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('first query');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -2957,7 +2957,7 @@ describe('useGeminiStream', () => {
|
||||
await result.current.submitQuery('second query');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -2980,7 +2980,7 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
|
||||
// Verify that the request was retried
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(3); // 1st query, 2nd query, retry of 2nd query
|
||||
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
@@ -3011,7 +3011,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Verify that the content was added to history before the loop detection dialog
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
@@ -3022,7 +3022,7 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Then verify loop detection confirmation request was set
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import type { MockedFunction } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useGitBranchName } from './useGitBranchName.js';
|
||||
import { fs, vol } from 'memfs';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
@@ -161,7 +162,7 @@ describe('useGitBranchName', () => {
|
||||
expect(result.current).toBe('main');
|
||||
|
||||
// Wait for watcher to be set up
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(watchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -171,7 +172,7 @@ describe('useGitBranchName', () => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('develop');
|
||||
});
|
||||
});
|
||||
@@ -236,7 +237,7 @@ describe('useGitBranchName', () => {
|
||||
});
|
||||
|
||||
// Wait for watcher to be set up BEFORE unmounting
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(watchMock).toHaveBeenCalledWith(
|
||||
GIT_LOGS_HEAD_PATH,
|
||||
expect.any(Function),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
@@ -78,34 +78,46 @@ describe('useIdeTrustListener', () => {
|
||||
);
|
||||
});
|
||||
|
||||
const renderTrustListenerHook = () => {
|
||||
const renderTrustListenerHook = async () => {
|
||||
let hookResult: ReturnType<typeof useIdeTrustListener>;
|
||||
function TestComponent() {
|
||||
hookResult = useIdeTrustListener();
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(<TestComponent />);
|
||||
const { rerender, unmount } = render(<TestComponent />);
|
||||
|
||||
// Flush any pending async state updates from the hook's initialization
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
rerender: async () => {
|
||||
rerender(<TestComponent />);
|
||||
},
|
||||
unmount: async () => {
|
||||
unmount();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize correctly with no trust information', () => {
|
||||
it('should initialize correctly with no trust information', async () => {
|
||||
vi.mocked(trustedFolders.isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: undefined,
|
||||
source: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderTrustListenerHook();
|
||||
const { result, unmount } = await renderTrustListenerHook();
|
||||
|
||||
expect(result.current.isIdeTrusted).toBe(undefined);
|
||||
expect(result.current.needsRestart).toBe(false);
|
||||
expect(result.current.restartReason).toBe('NONE');
|
||||
await unmount();
|
||||
});
|
||||
|
||||
it('should NOT set needsRestart when connecting for the first time', async () => {
|
||||
@@ -116,7 +128,7 @@ describe('useIdeTrustListener', () => {
|
||||
isTrusted: true,
|
||||
source: 'ide',
|
||||
});
|
||||
const { result } = renderTrustListenerHook();
|
||||
const { result, unmount } = await renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -136,6 +148,7 @@ describe('useIdeTrustListener', () => {
|
||||
expect(result.current.isIdeTrusted).toBe(true);
|
||||
expect(result.current.needsRestart).toBe(false);
|
||||
expect(result.current.restartReason).toBe('CONNECTION_CHANGE');
|
||||
await unmount();
|
||||
});
|
||||
|
||||
it('should set needsRestart when IDE trust changes', async () => {
|
||||
@@ -150,7 +163,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result } = renderTrustListenerHook();
|
||||
const { result, unmount } = await renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -174,6 +187,7 @@ describe('useIdeTrustListener', () => {
|
||||
expect(result.current.isIdeTrusted).toBe(false);
|
||||
expect(result.current.needsRestart).toBe(true);
|
||||
expect(result.current.restartReason).toBe('TRUST_CHANGE');
|
||||
await unmount();
|
||||
});
|
||||
|
||||
it('should set needsRestart when IDE disconnects', async () => {
|
||||
@@ -188,7 +202,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result } = renderTrustListenerHook();
|
||||
const { result, unmount } = await renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -210,6 +224,7 @@ describe('useIdeTrustListener', () => {
|
||||
expect(result.current.isIdeTrusted).toBe(undefined);
|
||||
expect(result.current.needsRestart).toBe(true);
|
||||
expect(result.current.restartReason).toBe('CONNECTION_CHANGE');
|
||||
await unmount();
|
||||
});
|
||||
|
||||
it('should NOT set needsRestart if trust value does not change', async () => {
|
||||
@@ -224,7 +239,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result, rerender } = renderTrustListenerHook();
|
||||
const { result, rerender, unmount } = await renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -234,9 +249,10 @@ describe('useIdeTrustListener', () => {
|
||||
expect(result.current.isIdeTrusted).toBe(true);
|
||||
expect(result.current.needsRestart).toBe(false);
|
||||
|
||||
rerender();
|
||||
await rerender();
|
||||
|
||||
expect(result.current.isIdeTrusted).toBe(true);
|
||||
expect(result.current.needsRestart).toBe(false);
|
||||
await unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { useStdin } from 'ink';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useLoadingIndicator } from './useLoadingIndicator.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { vi } from 'vitest';
|
||||
import {
|
||||
useMemoryMonitor,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useMessageQueue } from './useMessageQueue.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
|
||||
@@ -150,7 +151,7 @@ describe('useMessageQueue', () => {
|
||||
// Transition to Idle
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Message 1\n\nMessage 2');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
});
|
||||
@@ -206,7 +207,7 @@ describe('useMessageQueue', () => {
|
||||
// Go back to idle - should submit
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('First batch');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
});
|
||||
@@ -222,7 +223,7 @@ describe('useMessageQueue', () => {
|
||||
// Go back to idle - should submit again
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Second batch');
|
||||
expect(mockSubmitQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useModelCommand } from './useModelCommand.js';
|
||||
|
||||
describe('useModelCommand', () => {
|
||||
@@ -18,22 +18,24 @@ describe('useModelCommand', () => {
|
||||
}
|
||||
|
||||
it('should initialize with the model dialog closed', () => {
|
||||
render(<TestComponent />);
|
||||
const { unmount } = render(<TestComponent />);
|
||||
expect(result.isModelDialogOpen).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should open the model dialog when openModelDialog is called', () => {
|
||||
render(<TestComponent />);
|
||||
const { unmount } = render(<TestComponent />);
|
||||
|
||||
act(() => {
|
||||
result.openModelDialog();
|
||||
});
|
||||
|
||||
expect(result.isModelDialogOpen).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should close the model dialog when closeModelDialog is called', () => {
|
||||
render(<TestComponent />);
|
||||
const { unmount } = render(<TestComponent />);
|
||||
|
||||
// Open it first
|
||||
act(() => {
|
||||
@@ -46,5 +48,6 @@ describe('useModelCommand', () => {
|
||||
result.closeModelDialog();
|
||||
});
|
||||
expect(result.isModelDialogOpen).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import {
|
||||
usePhraseCycler,
|
||||
@@ -50,7 +50,9 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
rerender(<TestComponent isActive={true} isWaiting={true} />);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
});
|
||||
|
||||
@@ -59,7 +61,9 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent isActive={false} isWaiting={false} />,
|
||||
);
|
||||
const initialPhrase = lastFrame();
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS * 2);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS * 2);
|
||||
});
|
||||
expect(lastFrame()).toBe(initialPhrase);
|
||||
});
|
||||
|
||||
@@ -69,7 +73,9 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
// Initial phrase should be one of the witty phrases
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
|
||||
await act(async () => {
|
||||
@@ -77,7 +83,9 @@ describe('usePhraseCycler', () => {
|
||||
});
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
});
|
||||
|
||||
@@ -109,11 +117,15 @@ describe('usePhraseCycler', () => {
|
||||
customPhrases={customPhrases}
|
||||
/>,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Phrase A');
|
||||
|
||||
// Interval -> callCount 1 -> returns 0.99 -> 'Phrase B'
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
expect(lastFrame()).toBe('Phrase B');
|
||||
|
||||
// Deactivate -> resets to customPhrases[0] -> 'Phrase A'
|
||||
@@ -124,7 +136,9 @@ describe('usePhraseCycler', () => {
|
||||
customPhrases={customPhrases}
|
||||
/>,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Phrase A');
|
||||
|
||||
// Activate again -> callCount 2 -> returns 0 -> 'Phrase A'
|
||||
@@ -135,7 +149,9 @@ describe('usePhraseCycler', () => {
|
||||
customPhrases={customPhrases}
|
||||
/>,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Phrase A');
|
||||
});
|
||||
|
||||
@@ -164,7 +180,9 @@ describe('usePhraseCycler', () => {
|
||||
expect(lastFrame()).toBe('Custom Phrase 1');
|
||||
|
||||
randomMock.mockReturnValue(0.99);
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toBe('Custom Phrase 2');
|
||||
|
||||
@@ -175,7 +193,9 @@ describe('usePhraseCycler', () => {
|
||||
rerender(
|
||||
<TestComponent isActive={true} isWaiting={false} customPhrases={[]} />,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
});
|
||||
@@ -185,7 +205,9 @@ describe('usePhraseCycler', () => {
|
||||
const { lastFrame } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} customPhrases={[]} />,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
});
|
||||
@@ -195,22 +217,30 @@ describe('usePhraseCycler', () => {
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
|
||||
// Cycle to a different phrase (potentially)
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
|
||||
// Go to waiting state
|
||||
rerender(<TestComponent isActive={false} isWaiting={true} />);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
|
||||
// Go back to active cycling - should pick a random witty phrase
|
||||
rerender(<TestComponent isActive={true} isWaiting={false} />);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import type {
|
||||
Config,
|
||||
CodeAssistServer,
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { UserTierId, getCodeAssistServer } from '@google/gemini-cli-core';
|
||||
import { usePrivacySettings } from './usePrivacySettings.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -52,7 +54,7 @@ describe('usePrivacySettings', () => {
|
||||
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -71,7 +73,7 @@ describe('usePrivacySettings', () => {
|
||||
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -90,7 +92,7 @@ describe('usePrivacySettings', () => {
|
||||
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -118,15 +120,17 @@ describe('usePrivacySettings', () => {
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
// Wait for initial load
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Update the setting
|
||||
await result.current.updateDataCollectionOptIn(false);
|
||||
await act(async () => {
|
||||
await result.current.updateDataCollectionOptIn(false);
|
||||
});
|
||||
|
||||
// Wait for update to complete
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.privacyState.dataCollectionOptIn).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { renderHook } from '../../test-utils/render.js';
|
||||
import {
|
||||
type Config,
|
||||
type FallbackModelHandler,
|
||||
type FallbackIntent,
|
||||
UserTierId,
|
||||
AuthType,
|
||||
TerminalQuotaError,
|
||||
@@ -186,10 +187,13 @@ describe('useQuotaAndFallback', () => {
|
||||
} of testCases) {
|
||||
it(`should handle ${description} correctly`, async () => {
|
||||
const handler = getRegisteredHandler(tier);
|
||||
const result = await handler('model-A', 'model-B', error);
|
||||
let result: FallbackIntent | null;
|
||||
await act(async () => {
|
||||
result = await handler('model-A', 'model-B', error);
|
||||
});
|
||||
|
||||
// Automatic fallbacks should return 'stop'
|
||||
expect(result).toBe('stop');
|
||||
expect(result!).toBe('stop');
|
||||
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: MessageType.INFO }),
|
||||
@@ -224,25 +228,26 @@ describe('useQuotaAndFallback', () => {
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
// Call the handler but do not await it, to check the intermediate state
|
||||
const promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
});
|
||||
|
||||
// The hook should now have a pending request for the UI to handle
|
||||
expect(result.current.proQuotaRequest).not.toBeNull();
|
||||
expect(result.current.proQuotaRequest?.failedModel).toBe('gemini-pro');
|
||||
|
||||
// Simulate the user choosing to continue with the fallback model
|
||||
act(() => {
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('continue');
|
||||
});
|
||||
|
||||
// The original promise from the handler should now resolve
|
||||
const intent = await promise;
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry');
|
||||
|
||||
// The pending request should be cleared from the state
|
||||
@@ -263,31 +268,36 @@ describe('useQuotaAndFallback', () => {
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
const promise1 = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota 1', mockGoogleApiError),
|
||||
);
|
||||
await act(async () => {});
|
||||
let promise1: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise1 = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota 1', mockGoogleApiError),
|
||||
);
|
||||
});
|
||||
|
||||
const firstRequest = result.current.proQuotaRequest;
|
||||
expect(firstRequest).not.toBeNull();
|
||||
|
||||
const result2 = await handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota 2', mockGoogleApiError),
|
||||
);
|
||||
let result2: FallbackIntent | null;
|
||||
await act(async () => {
|
||||
result2 = await handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota 2', mockGoogleApiError),
|
||||
);
|
||||
});
|
||||
|
||||
// The lock should have stopped the second request
|
||||
expect(result2).toBe('stop');
|
||||
expect(result2!).toBe('stop');
|
||||
expect(result.current.proQuotaRequest).toBe(firstRequest);
|
||||
|
||||
act(() => {
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('continue');
|
||||
});
|
||||
|
||||
const intent1 = await promise1;
|
||||
const intent1 = await promise1!;
|
||||
expect(intent1).toBe('retry');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
});
|
||||
@@ -327,18 +337,20 @@ describe('useQuotaAndFallback', () => {
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
const promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
await act(async () => {}); // Allow state to update
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('auth');
|
||||
});
|
||||
|
||||
const intent = await promise;
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('auth');
|
||||
expect(mockSetAuthState).toHaveBeenCalledWith(AuthState.Updating);
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
@@ -358,18 +370,20 @@ describe('useQuotaAndFallback', () => {
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
// The first `addItem` call is for the initial quota error message
|
||||
const promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
await act(async () => {}); // Allow state to update
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('continue');
|
||||
});
|
||||
|
||||
const intent = await promise;
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import {
|
||||
useSelectionList,
|
||||
type SelectionListItem,
|
||||
@@ -67,7 +68,7 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const renderSelectionListHook = (initialProps: {
|
||||
const renderSelectionListHook = async (initialProps: {
|
||||
items: Array<SelectionListItem<string>>;
|
||||
onSelect: (item: string) => void;
|
||||
onHighlight?: (item: string) => void;
|
||||
@@ -87,23 +88,26 @@ describe('useSelectionList', () => {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: Partial<typeof initialProps>) =>
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />),
|
||||
unmount,
|
||||
rerender: async (newProps: Partial<typeof initialProps>) => {
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />);
|
||||
},
|
||||
unmount: async () => {
|
||||
unmount();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should initialize with the default index (0) if enabled', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should initialize with the default index (0) if enabled', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize with the provided initialIndex if enabled', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should initialize with the provided initialIndex if enabled', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -111,16 +115,16 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle an empty list gracefully', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should handle an empty list gracefully', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: [],
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should find the next enabled item (downwards) if initialIndex is disabled', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should find the next enabled item (downwards) if initialIndex is disabled', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -128,13 +132,13 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should wrap around to find the next enabled item if initialIndex is disabled', () => {
|
||||
it('should wrap around to find the next enabled item if initialIndex is disabled', async () => {
|
||||
const wrappingItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', disabled: true, key: 'C' },
|
||||
];
|
||||
const { result } = renderSelectionListHook({
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: wrappingItems,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -142,15 +146,15 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should default to 0 if initialIndex is out of bounds', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should default to 0 if initialIndex is out of bounds', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 10,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
|
||||
const { result: resultNeg } = renderSelectionListHook({
|
||||
const { result: resultNeg } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: -1,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -158,12 +162,12 @@ describe('useSelectionList', () => {
|
||||
expect(resultNeg.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should stick to the initial index if all items are disabled', () => {
|
||||
it('should stick to the initial index if all items are disabled', async () => {
|
||||
const allDisabled = [
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderSelectionListHook({
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: allDisabled,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -173,8 +177,8 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
describe('Keyboard Navigation (Up/Down/J/K)', () => {
|
||||
it('should move down with "j" and "down" keys, skipping disabled items', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should move down with "j" and "down" keys, skipping disabled items', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
@@ -185,8 +189,8 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('should move up with "k" and "up" keys, skipping disabled items', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should move up with "k" and "up" keys, skipping disabled items', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 3,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -198,8 +202,8 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should wrap navigation correctly', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should wrap navigation correctly', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: items.length - 1,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -212,8 +216,8 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('should call onHighlight when index changes', () => {
|
||||
renderSelectionListHook({
|
||||
it('should call onHighlight when index changes', async () => {
|
||||
await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -223,9 +227,9 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnHighlight).toHaveBeenCalledWith('C');
|
||||
});
|
||||
|
||||
it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', () => {
|
||||
it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', async () => {
|
||||
const singleItem = [{ value: 'A', key: 'A' }];
|
||||
const { result } = renderSelectionListHook({
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: singleItem,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -235,12 +239,12 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnHighlight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not move or call onHighlight if all items are disabled', () => {
|
||||
it('should not move or call onHighlight if all items are disabled', async () => {
|
||||
const allDisabled = [
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderSelectionListHook({
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: allDisabled,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -253,8 +257,8 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
describe('Selection (Enter)', () => {
|
||||
it('should call onSelect when "return" is pressed on enabled item', () => {
|
||||
renderSelectionListHook({
|
||||
it('should call onSelect when "return" is pressed on enabled item', async () => {
|
||||
await renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
@@ -264,8 +268,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('C');
|
||||
});
|
||||
|
||||
it('should not call onSelect if the active item is disabled', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should not call onSelect if the active item is disabled', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
@@ -278,8 +282,8 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
describe('Keyboard Navigation Robustness (Rapid Input)', () => {
|
||||
it('should handle rapid navigation and selection robustly (avoiding stale state)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should handle rapid navigation and selection robustly (avoiding stale state)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -326,8 +330,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).not.toHaveBeenCalledWith('A');
|
||||
});
|
||||
|
||||
it('should handle ultra-rapid input (multiple presses in single act) without stale state', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should handle ultra-rapid input (multiple presses in single act) without stale state', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -366,8 +370,8 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
describe('Focus Management (isFocused)', () => {
|
||||
it('should activate the keypress handler when focused (default) and items exist', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should activate the keypress handler when focused (default) and items exist', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
@@ -376,8 +380,8 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should not activate the keypress handler when isFocused is false', () => {
|
||||
renderSelectionListHook({
|
||||
it('should not activate the keypress handler when isFocused is false', async () => {
|
||||
await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: false,
|
||||
@@ -386,8 +390,8 @@ describe('useSelectionList', () => {
|
||||
expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
|
||||
});
|
||||
|
||||
it('should not activate the keypress handler when items list is empty', () => {
|
||||
renderSelectionListHook({
|
||||
it('should not activate the keypress handler when items list is empty', async () => {
|
||||
await renderSelectionListHook({
|
||||
items: [],
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: true,
|
||||
@@ -396,8 +400,8 @@ describe('useSelectionList', () => {
|
||||
expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
|
||||
});
|
||||
|
||||
it('should activate/deactivate when isFocused prop changes', () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
it('should activate/deactivate when isFocused prop changes', async () => {
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: false,
|
||||
@@ -405,12 +409,12 @@ describe('useSelectionList', () => {
|
||||
|
||||
expect(activeKeypressHandler).toBeNull();
|
||||
|
||||
rerender({ isFocused: true });
|
||||
await rerender({ isFocused: true });
|
||||
expect(activeKeypressHandler).not.toBeNull();
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
|
||||
rerender({ isFocused: false });
|
||||
await rerender({ isFocused: false });
|
||||
expect(activeKeypressHandler).toBeNull();
|
||||
expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
|
||||
});
|
||||
@@ -433,8 +437,8 @@ describe('useSelectionList', () => {
|
||||
|
||||
const pressNumber = (num: string) => pressKey(num, num);
|
||||
|
||||
it('should not respond to numbers if showNumbers is false (default)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should not respond to numbers if showNumbers is false (default)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
@@ -443,8 +447,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should select item immediately if the number cannot be extended (unambiguous)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should select item immediately if the number cannot be extended (unambiguous)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -459,8 +463,8 @@ describe('useSelectionList', () => {
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should highlight and wait for timeout if the number can be extended (ambiguous)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should highlight and wait for timeout if the number can be extended (ambiguous)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: longList,
|
||||
initialIndex: 1, // Start at index 1 so pressing "1" (index 0) causes a change
|
||||
onSelect: mockOnSelect,
|
||||
@@ -484,8 +488,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
|
||||
});
|
||||
|
||||
it('should handle multi-digit input correctly', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should handle multi-digit input correctly', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -502,8 +506,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('Item 12');
|
||||
});
|
||||
|
||||
it('should reset buffer if input becomes invalid (out of bounds)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should reset buffer if input becomes invalid (out of bounds)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -519,8 +523,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('C');
|
||||
});
|
||||
|
||||
it('should allow "0" as subsequent digit, but ignore as first digit', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should allow "0" as subsequent digit, but ignore as first digit', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -540,8 +544,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('Item 10');
|
||||
});
|
||||
|
||||
it('should clear the initial "0" input after timeout', () => {
|
||||
renderSelectionListHook({
|
||||
it('should clear the initial "0" input after timeout', async () => {
|
||||
await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -557,8 +561,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
|
||||
});
|
||||
|
||||
it('should highlight but not select a disabled item (immediate selection case)', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should highlight but not select a disabled item (immediate selection case)', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: shortList, // B (index 1, number 2) is disabled
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -574,14 +578,14 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should highlight but not select a disabled item (timeout case)', () => {
|
||||
it('should highlight but not select a disabled item (timeout case)', async () => {
|
||||
// Create a list where the ambiguous prefix points to a disabled item
|
||||
const disabledAmbiguousList = [
|
||||
{ value: 'Item 1 Disabled', disabled: true, key: 'Item 1 Disabled' },
|
||||
...longList.slice(1),
|
||||
];
|
||||
|
||||
const { result } = renderSelectionListHook({
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: disabledAmbiguousList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -599,8 +603,8 @@ describe('useSelectionList', () => {
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear the number buffer if a non-numeric key (e.g., navigation) is pressed', () => {
|
||||
const { result } = renderSelectionListHook({
|
||||
it('should clear the number buffer if a non-numeric key (e.g., navigation) is pressed', async () => {
|
||||
const { result } = await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -619,8 +623,8 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should clear the number buffer if "return" is pressed', () => {
|
||||
renderSelectionListHook({
|
||||
it('should clear the number buffer if "return" is pressed', async () => {
|
||||
await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -642,20 +646,20 @@ describe('useSelectionList', () => {
|
||||
|
||||
describe('Reactivity (Dynamic Updates)', () => {
|
||||
it('should update activeIndex when initialIndex prop changes', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
rerender({ initialIndex: 2 });
|
||||
await vi.waitFor(() => {
|
||||
await rerender({ initialIndex: 2 });
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a new initialIndex even after user interaction', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
@@ -666,30 +670,30 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
|
||||
// The component re-renders with a new initial index
|
||||
rerender({ initialIndex: 3 });
|
||||
await rerender({ initialIndex: 3 });
|
||||
|
||||
// The hook should now respect the new initial index
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate index when initialIndex prop changes to a disabled item', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
rerender({ initialIndex: 1 });
|
||||
await rerender({ initialIndex: 1 });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should adjust activeIndex if items change and the initialIndex is now out of bounds', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 3,
|
||||
items,
|
||||
@@ -701,10 +705,10 @@ describe('useSelectionList', () => {
|
||||
{ value: 'X', key: 'X' },
|
||||
{ value: 'Y', key: 'Y' },
|
||||
];
|
||||
rerender({ items: shorterItems }); // Length 2
|
||||
await rerender({ items: shorterItems }); // Length 2
|
||||
|
||||
// The useEffect syncs based on the initialIndex (3) which is now out of bounds. It defaults to 0.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -715,7 +719,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: initialItems,
|
||||
@@ -728,22 +732,22 @@ describe('useSelectionList', () => {
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset to 0 if items change to an empty list', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 2,
|
||||
items,
|
||||
});
|
||||
|
||||
rerender({ items: [] });
|
||||
await vi.waitFor(() => {
|
||||
await rerender({ items: [] });
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -756,7 +760,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 2,
|
||||
@@ -780,10 +784,10 @@ describe('useSelectionList', () => {
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
|
||||
// Active index should remain the same since items are deeply equal
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
// onHighlight should NOT be called since the index didn't change
|
||||
@@ -798,7 +802,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 3,
|
||||
@@ -815,10 +819,10 @@ describe('useSelectionList', () => {
|
||||
{ value: 'Z', key: 'Z' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
|
||||
// Active index should update based on initialIndex and new items
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -830,7 +834,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: initialItems,
|
||||
@@ -845,10 +849,10 @@ describe('useSelectionList', () => {
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
|
||||
// Should find next valid index since current became disabled
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -860,7 +864,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
const { result, rerender } = await renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
items: initialItems,
|
||||
});
|
||||
@@ -875,14 +879,14 @@ describe('useSelectionList', () => {
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not re-initialize when items have identical keys but are different objects', () => {
|
||||
it('should not re-initialize when items have identical keys but are different objects', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
@@ -890,7 +894,7 @@ describe('useSelectionList', () => {
|
||||
|
||||
let renderCount = 0;
|
||||
|
||||
const renderHookWithCount = (initialProps: {
|
||||
const renderHookWithCount = async (initialProps: {
|
||||
items: Array<SelectionListItem<string>>;
|
||||
}) => {
|
||||
function TestComponent(props: typeof initialProps) {
|
||||
@@ -904,12 +908,13 @@ describe('useSelectionList', () => {
|
||||
}
|
||||
const { rerender } = render(<TestComponent {...initialProps} />);
|
||||
return {
|
||||
rerender: (newProps: Partial<typeof initialProps>) =>
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />),
|
||||
rerender: async (newProps: Partial<typeof initialProps>) => {
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const { rerender } = renderHookWithCount({ items: initialItems });
|
||||
const { rerender } = await renderHookWithCount({ items: initialItems });
|
||||
|
||||
// Initial render
|
||||
expect(renderCount).toBe(1);
|
||||
@@ -920,7 +925,7 @@ describe('useSelectionList', () => {
|
||||
{ value: 'B', key: 'B' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
await rerender({ items: newItems });
|
||||
expect(renderCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -934,13 +939,13 @@ describe('useSelectionList', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should clear timeout on unmount when timer is active', () => {
|
||||
it('should clear timeout on unmount when timer is active', async () => {
|
||||
const longList: Array<SelectionListItem<string>> = Array.from(
|
||||
{ length: 15 },
|
||||
(_, i) => ({ value: `Item ${i + 1}`, key: `Item ${i + 1}` }),
|
||||
);
|
||||
|
||||
const { unmount } = renderSelectionListHook({
|
||||
const { unmount } = await renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
@@ -955,7 +960,7 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
await unmount();
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useShellHistory } from './useShellHistory.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
@@ -96,9 +97,11 @@ describe('useShellHistory', () => {
|
||||
|
||||
it('should initialize and read the history file from the correct path', async () => {
|
||||
mockedFs.readFile.mockResolvedValue('cmd1\ncmd2');
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalledWith(
|
||||
MOCKED_HISTORY_FILE,
|
||||
'utf-8',
|
||||
@@ -112,6 +115,8 @@ describe('useShellHistory', () => {
|
||||
|
||||
// History is loaded newest-first: ['cmd2', 'cmd1']
|
||||
expect(command).toBe('cmd2');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle a nonexistent history file gracefully', async () => {
|
||||
@@ -119,9 +124,11 @@ describe('useShellHistory', () => {
|
||||
error.code = 'ENOENT';
|
||||
mockedFs.readFile.mockRejectedValue(error);
|
||||
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -131,12 +138,16 @@ describe('useShellHistory', () => {
|
||||
});
|
||||
|
||||
expect(command).toBe(null);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should add a command and write to the history file', async () => {
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -144,7 +155,7 @@ describe('useShellHistory', () => {
|
||||
result.current.addCommandToHistory('new_command');
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.mkdir).toHaveBeenCalledWith(MOCKED_HISTORY_DIR, {
|
||||
recursive: true,
|
||||
});
|
||||
@@ -159,14 +170,18 @@ describe('useShellHistory', () => {
|
||||
command = result.current.getPreviousCommand();
|
||||
});
|
||||
expect(command).toBe('new_command');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should navigate history correctly with previous/next commands', async () => {
|
||||
mockedFs.readFile.mockResolvedValue('cmd1\ncmd2\ncmd3');
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
// Wait for history to be loaded: ['cmd3', 'cmd2', 'cmd1']
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -208,12 +223,16 @@ describe('useShellHistory', () => {
|
||||
command = result.current.getNextCommand();
|
||||
});
|
||||
expect(command).toBe('');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not add empty or whitespace-only commands to history', async () => {
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -222,14 +241,18 @@ describe('useShellHistory', () => {
|
||||
});
|
||||
|
||||
expect(mockedFs.writeFile).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should truncate history to MAX_HISTORY_LENGTH (100)', async () => {
|
||||
const oldCommands = Array.from({ length: 120 }, (_, i) => `old_cmd_${i}`);
|
||||
mockedFs.readFile.mockResolvedValue(oldCommands.join('\n'));
|
||||
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
await vi.waitFor(() => {
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -238,7 +261,7 @@ describe('useShellHistory', () => {
|
||||
});
|
||||
|
||||
// Wait for the async write to happen and then inspect the arguments.
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -252,14 +275,18 @@ describe('useShellHistory', () => {
|
||||
expect(writtenLines.length).toBe(100);
|
||||
expect(writtenLines[0]).toBe('old_cmd_21'); // New oldest command
|
||||
expect(writtenLines[99]).toBe('new_cmd'); // Newest command
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should move an existing command to the top when re-added', async () => {
|
||||
mockedFs.readFile.mockResolvedValue('cmd1\ncmd2\ncmd3');
|
||||
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useShellHistory(MOCKED_PROJECT_ROOT),
|
||||
);
|
||||
|
||||
// Initial state: ['cmd3', 'cmd2', 'cmd1']
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -270,7 +297,7 @@ describe('useShellHistory', () => {
|
||||
// After re-adding 'cmd1': ['cmd1', 'cmd3', 'cmd2']
|
||||
expect(mockedFs.readFile).toHaveBeenCalled();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(mockedFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -278,5 +305,7 @@ describe('useShellHistory', () => {
|
||||
const writtenLines = writtenContent.split('\n');
|
||||
|
||||
expect(writtenLines).toEqual(['cmd2', 'cmd3', 'cmd1']);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { act, useState } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useSlashCompletion } from './useSlashCompletion.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
import { useState } from 'react';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
|
||||
// Test utility type and helper function for creating test SlashCommands
|
||||
@@ -194,46 +195,86 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
createTestCommand({ name: 'chat', description: 'Manage chat history' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(slashCommands.length);
|
||||
expect(result.current.suggestions.map((s) => s.label)).toEqual(
|
||||
expect.arrayContaining(['help', 'clear', 'memory', 'chat', 'stats']),
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(slashCommands.length);
|
||||
expect(result.current.suggestions.map((s) => s.label)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'help',
|
||||
'clear',
|
||||
'memory',
|
||||
'chat',
|
||||
'stats',
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should filter commands based on partial input', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({ name: 'memory', description: 'Manage memory' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/mem',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
const setSuggestions = vi.fn();
|
||||
const setIsLoadingSuggestions = vi.fn();
|
||||
const setIsPerfectMatch = vi.fn();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'memory',
|
||||
value: 'memory',
|
||||
description: 'Manage memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
let result: {
|
||||
current: { completionStart: number; completionEnd: number };
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useSlashCompletion({
|
||||
enabled: true,
|
||||
query: '/mem',
|
||||
slashCommands,
|
||||
commandContext: mockCommandContext,
|
||||
setSuggestions,
|
||||
setIsLoadingSuggestions,
|
||||
setIsPerfectMatch,
|
||||
}),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(setSuggestions).toHaveBeenCalledWith([
|
||||
{
|
||||
label: 'memory',
|
||||
value: 'memory',
|
||||
description: 'Manage memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
expect(result.current.completionEnd).toBe(4);
|
||||
});
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should suggest commands based on partial altNames', async () => {
|
||||
@@ -244,16 +285,24 @@ describe('useSlashCompletion', () => {
|
||||
description: 'check session stats. Usage: /stats [model|tools]',
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/usag',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/usag',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'stats',
|
||||
@@ -262,7 +311,9 @@ describe('useSlashCompletion', () => {
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should NOT provide suggestions for a perfectly typed command that is a leaf node', async () => {
|
||||
@@ -273,16 +324,27 @@ describe('useSlashCompletion', () => {
|
||||
action: vi.fn(),
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/clear',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/clear',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it.each([['/?'], ['/usage']])(
|
||||
@@ -303,16 +365,28 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
query,
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
query,
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount!();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -320,32 +394,55 @@ describe('useSlashCompletion', () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({ name: 'clear', description: 'Clear the screen' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/clear ',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/clear ',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should not provide suggestions for an unknown command', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({ name: 'help', description: 'Show help' }),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/unknown-command',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/unknown-command',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should not suggest hidden commands', async () => {
|
||||
@@ -360,19 +457,28 @@ describe('useSlashCompletion', () => {
|
||||
hidden: true,
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
let result: {
|
||||
current: ReturnType<typeof useTestHarnessForSlashCompletion>;
|
||||
};
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const hook = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
result = hook.result;
|
||||
unmount = hook.unmount;
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
expect(result.current.suggestions[0].label).toBe('visible');
|
||||
});
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,7 +495,7 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory ',
|
||||
@@ -398,7 +504,7 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(2);
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -417,6 +523,7 @@ describe('useSlashCompletion', () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should suggest all sub-commands when the query ends with the parent command and a space', async () => {
|
||||
@@ -430,7 +537,7 @@ describe('useSlashCompletion', () => {
|
||||
],
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory ',
|
||||
@@ -439,7 +546,7 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(2);
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -458,6 +565,7 @@ describe('useSlashCompletion', () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should filter sub-commands by prefix', async () => {
|
||||
@@ -471,7 +579,7 @@ describe('useSlashCompletion', () => {
|
||||
],
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory a',
|
||||
@@ -480,7 +588,7 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'add',
|
||||
@@ -489,7 +597,9 @@ describe('useSlashCompletion', () => {
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(8);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should provide no suggestions for an invalid sub-command', async () => {
|
||||
@@ -503,7 +613,7 @@ describe('useSlashCompletion', () => {
|
||||
],
|
||||
}),
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory dothisnow',
|
||||
@@ -511,8 +621,13 @@ describe('useSlashCompletion', () => {
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
expect(result.current.completionStart).toBe(8);
|
||||
});
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -544,7 +659,7 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/chat resume my-ch',
|
||||
@@ -553,25 +668,32 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume my-ch',
|
||||
name: 'resume',
|
||||
args: 'my-ch',
|
||||
},
|
||||
}),
|
||||
'my-ch',
|
||||
);
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume my-ch',
|
||||
name: 'resume',
|
||||
args: 'my-ch',
|
||||
},
|
||||
}),
|
||||
'my-ch',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{ label: 'my-chat-tag-1', value: 'my-chat-tag-1' },
|
||||
{ label: 'my-chat-tag-2', value: 'my-chat-tag-2' },
|
||||
]);
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{ label: 'my-chat-tag-1', value: 'my-chat-tag-1' },
|
||||
{ label: 'my-chat-tag-2', value: 'my-chat-tag-2' },
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(13);
|
||||
expect(result.current.isLoadingSuggestions).toBe(false);
|
||||
});
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call command.completion with an empty string when args start with a space', async () => {
|
||||
@@ -593,7 +715,7 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/chat resume ',
|
||||
@@ -602,50 +724,55 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume',
|
||||
name: 'resume',
|
||||
args: '',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
);
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockCompletionFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocation: {
|
||||
raw: '/chat resume ',
|
||||
name: 'resume',
|
||||
args: '',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(3);
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(3);
|
||||
expect(result.current.completionStart).toBe(13);
|
||||
});
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle completion function that returns null', async () => {
|
||||
const completionFn = vi.fn().mockResolvedValue(null);
|
||||
const mockCompletionFn = vi.fn().mockResolvedValue(null);
|
||||
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
description: 'Manage chat history',
|
||||
subCommands: [
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume a saved chat',
|
||||
completion: completionFn,
|
||||
}),
|
||||
],
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
completion: mockCompletionFn,
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/chat resume ',
|
||||
'/test arg',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toHaveLength(0);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([]);
|
||||
expect(result.current.isLoadingSuggestions).toBe(false);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -666,7 +793,7 @@ describe('useSlashCompletion', () => {
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/',
|
||||
@@ -675,22 +802,25 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'summarize',
|
||||
value: 'summarize',
|
||||
description: 'Summarize content',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
{
|
||||
label: 'help',
|
||||
value: 'help',
|
||||
description: 'Show help',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'summarize',
|
||||
value: 'summarize',
|
||||
description: 'Summarize content',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
{
|
||||
label: 'help',
|
||||
value: 'help',
|
||||
description: 'Show help',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should include commandKind when filtering MCP commands by prefix', async () => {
|
||||
@@ -709,7 +839,7 @@ describe('useSlashCompletion', () => {
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/summ',
|
||||
@@ -718,7 +848,7 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'summarize',
|
||||
@@ -727,7 +857,9 @@ describe('useSlashCompletion', () => {
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should include commandKind for sub-commands', async () => {
|
||||
@@ -753,7 +885,7 @@ describe('useSlashCompletion', () => {
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/memory',
|
||||
@@ -762,22 +894,25 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'show',
|
||||
value: 'show',
|
||||
description: 'Show memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
]),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
label: 'show',
|
||||
value: 'show',
|
||||
description: 'Show memory',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
},
|
||||
{
|
||||
label: 'add',
|
||||
value: 'add',
|
||||
description: 'Add to memory',
|
||||
commandKind: CommandKind.MCP_PROMPT,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should include commandKind for file commands', async () => {
|
||||
@@ -790,7 +925,7 @@ describe('useSlashCompletion', () => {
|
||||
},
|
||||
] as SlashCommand[];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/custom',
|
||||
@@ -799,7 +934,7 @@ describe('useSlashCompletion', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'custom-script',
|
||||
@@ -808,11 +943,13 @@ describe('useSlashCompletion', () => {
|
||||
commandKind: CommandKind.FILE,
|
||||
},
|
||||
]);
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call shared callbacks when disabled', () => {
|
||||
it('should not call shared callbacks when disabled', async () => {
|
||||
const mockSetSuggestions = vi.fn();
|
||||
const mockSetIsLoadingSuggestions = vi.fn();
|
||||
const mockSetIsPerfectMatch = vi.fn();
|
||||
@@ -824,7 +961,7 @@ describe('useSlashCompletion', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const { rerender } = renderHook(
|
||||
const { rerender, unmount } = renderHook(
|
||||
({ enabled, query }) =>
|
||||
useSlashCompletion({
|
||||
enabled,
|
||||
@@ -849,9 +986,17 @@ describe('useSlashCompletion', () => {
|
||||
rerender({ enabled: false, query: '@src/file.ts' });
|
||||
rerender({ enabled: false, query: '@src/file.tsx' });
|
||||
|
||||
// Wait for any internal async operations to settle to avoid act warnings
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Should not have called shared callbacks during @ completion typing
|
||||
expect(mockSetSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsLoadingSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsPerfectMatch).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(mockSetSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsLoadingSuggestions).not.toHaveBeenCalled();
|
||||
expect(mockSetIsPerfectMatch).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,6 +159,7 @@ interface PerfectMatchResult {
|
||||
}
|
||||
|
||||
function useCommandSuggestions(
|
||||
query: string | null,
|
||||
parserResult: CommandParserResult,
|
||||
commandContext: CommandContext,
|
||||
getFzfForCommands: (
|
||||
@@ -207,7 +208,7 @@ function useCommandSuggestions(
|
||||
{
|
||||
...commandContext,
|
||||
invocation: {
|
||||
raw: `/${rawParts.join(' ')}`,
|
||||
raw: query || `/${rawParts.join(' ')}`,
|
||||
name: leafCommand.name,
|
||||
args: argString,
|
||||
},
|
||||
@@ -306,7 +307,13 @@ function useCommandSuggestions(
|
||||
|
||||
setSuggestions([]);
|
||||
return () => abortController.abort();
|
||||
}, [parserResult, commandContext, getFzfForCommands, getPrefixSuggestions]);
|
||||
}, [
|
||||
query,
|
||||
parserResult,
|
||||
commandContext,
|
||||
getFzfForCommands,
|
||||
getPrefixSuggestions,
|
||||
]);
|
||||
|
||||
return { suggestions, isLoading };
|
||||
}
|
||||
@@ -475,6 +482,7 @@ export function useSlashCompletion(props: UseSlashCompletionProps): {
|
||||
// Use extracted hooks for better separation of concerns
|
||||
const parserResult = useCommandParser(query, slashCommands);
|
||||
const { suggestions: hookSuggestions, isLoading } = useCommandSuggestions(
|
||||
query,
|
||||
parserResult,
|
||||
commandContext,
|
||||
getFzfForCommands,
|
||||
@@ -503,7 +511,11 @@ export function useSlashCompletion(props: UseSlashCompletionProps): {
|
||||
return;
|
||||
}
|
||||
|
||||
setSuggestions(hookSuggestions);
|
||||
if (isPerfectMatch) {
|
||||
setSuggestions([]);
|
||||
} else {
|
||||
setSuggestions(hookSuggestions);
|
||||
}
|
||||
setIsLoadingSuggestions(isLoading);
|
||||
setIsPerfectMatch(isPerfectMatch);
|
||||
setCompletionStart(calculatedStart);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useTimer } from './useTimer.js';
|
||||
|
||||
describe('useTimer', () => {
|
||||
|
||||
@@ -407,7 +407,9 @@ describe('useReactToolScheduler', () => {
|
||||
]);
|
||||
|
||||
// Clean up the pending promise to avoid open handles.
|
||||
resolveExecute({ llmContent: 'output', returnDisplay: 'display' });
|
||||
await act(async () => {
|
||||
resolveExecute({ llmContent: 'output', returnDisplay: 'display' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tool not found', async () => {
|
||||
@@ -854,7 +856,9 @@ describe('useReactToolScheduler', () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
schedule(request2, new AbortController().signal);
|
||||
act(() => {
|
||||
schedule(request2, new AbortController().signal);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useVim } from './vim.js';
|
||||
import type { VimMode } from './vim.js';
|
||||
import type { Key } from './useKeypress.js';
|
||||
@@ -175,25 +176,10 @@ describe('useVim hook', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const renderVimHook = (buffer?: Partial<TextBuffer>) => {
|
||||
let hookResult: ReturnType<typeof useVim>;
|
||||
function TestComponent() {
|
||||
hookResult = useVim(
|
||||
(buffer || mockBuffer) as TextBuffer,
|
||||
mockHandleFinalSubmit,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
};
|
||||
};
|
||||
const renderVimHook = (buffer?: Partial<TextBuffer>) =>
|
||||
renderHook(() =>
|
||||
useVim((buffer || mockBuffer) as TextBuffer, mockHandleFinalSubmit),
|
||||
);
|
||||
|
||||
const exitInsertMode = (result: {
|
||||
current: {
|
||||
@@ -1307,7 +1293,7 @@ describe('useVim hook', () => {
|
||||
mockVimContext.vimMode = 'INSERT';
|
||||
const { result } = renderVimHook();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
@@ -1323,7 +1309,7 @@ describe('useVim hook', () => {
|
||||
const emptyBuffer = createMockBuffer('');
|
||||
const { result } = renderVimHook(emptyBuffer);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
@@ -1337,7 +1323,7 @@ describe('useVim hook', () => {
|
||||
const nonEmptyBuffer = createMockBuffer('not empty');
|
||||
const { result } = renderVimHook(nonEmptyBuffer);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,61 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, beforeEach, afterEach } from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
|
||||
global.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
// Unset NO_COLOR environment variable to ensure consistent theme behavior between local and CI test runs
|
||||
if (process.env.NO_COLOR !== undefined) {
|
||||
delete process.env.NO_COLOR;
|
||||
}
|
||||
|
||||
import './src/test-utils/customMatchers.js';
|
||||
|
||||
let consoleErrorSpy: vi.SpyInstance;
|
||||
let actWarnings: Array<{ message: string; stack: string }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
actWarnings = [];
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
||||
const firstArg = args[0];
|
||||
if (
|
||||
typeof firstArg === 'string' &&
|
||||
firstArg.includes('was not wrapped in act(...)')
|
||||
) {
|
||||
const stackLines = (new Error().stack || '').split('\n');
|
||||
let lastReactFrameIndex = -1;
|
||||
|
||||
// Find the index of the last frame that comes from react-reconciler
|
||||
for (let i = 0; i < stackLines.length; i++) {
|
||||
if (stackLines[i].includes('react-reconciler')) {
|
||||
lastReactFrameIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found react-reconciler frames, start the stack trace after the last one.
|
||||
// Otherwise, just strip the first line (which is the Error message itself).
|
||||
const relevantStack =
|
||||
lastReactFrameIndex !== -1
|
||||
? stackLines.slice(lastReactFrameIndex + 1).join('\n')
|
||||
: stackLines.slice(1).join('\n');
|
||||
|
||||
actWarnings.push({
|
||||
message: format(...args),
|
||||
stack: relevantStack,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
|
||||
if (actWarnings.length > 0) {
|
||||
const messages = actWarnings
|
||||
.map(({ message, stack }) => `${message}\n${stack}`)
|
||||
.join('\n\n');
|
||||
throw new Error(`Failing test due to "act(...)" warnings:\n${messages}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,13 +12,16 @@ import * as path from 'node:path';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
conditions: ['test'],
|
||||
},
|
||||
test: {
|
||||
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', 'config.test.ts'],
|
||||
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}', 'config.test.ts'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**'],
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
reporters: ['default', 'junit'],
|
||||
silent: true,
|
||||
|
||||
outputFile: {
|
||||
junit: 'junit.xml',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user