mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
First batch of fixing tests to use best practices. (#11964)
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { vi, type MockedFunction } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
@@ -460,8 +462,7 @@ describe('extension tests', () => {
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
|
||||
),
|
||||
@@ -492,8 +493,7 @@ describe('extension tests', () => {
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
),
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { vi, type MockedFunction } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
|
||||
@@ -377,8 +377,7 @@ describe('validateDnsResolutionOrder', () => {
|
||||
|
||||
it('should return the default "ipv4first" and log a warning for an invalid string', () => {
|
||||
expect(validateDnsResolutionOrder('invalid-value')).toBe('ipv4first');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect(consoleWarnSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
'Invalid value for dnsResolutionOrder in settings: "invalid-value". Using default "ipv4first".',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor, act } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import type { InputPromptProps } from './InputPrompt.js';
|
||||
import { InputPrompt } from './InputPrompt.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
@@ -1936,7 +1936,7 @@ describe('InputPrompt', () => {
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
callback('Message 1\n\nMessage 2\n\nMessage 3');
|
||||
});
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith(
|
||||
@@ -1978,7 +1978,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
callback(undefined);
|
||||
});
|
||||
|
||||
@@ -2021,7 +2021,7 @@ describe('InputPrompt', () => {
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
callback('Single message');
|
||||
});
|
||||
|
||||
@@ -2077,7 +2077,7 @@ describe('InputPrompt', () => {
|
||||
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
|
||||
|
||||
const callback = mockPopAllMessages.mock.calls[0][0];
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
callback(undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { render, cleanup } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
|
||||
@@ -12,7 +12,6 @@ import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import { DEFAULT_THEME, themeManager } from '../themes/theme-manager.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
const createMockSettings = (
|
||||
userSettings = {},
|
||||
@@ -127,7 +126,7 @@ describe('ThemeDialog Snapshots', () => {
|
||||
stdin.write('\x1b');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(mockOnCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-collapsed-match 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ (r:) Type your message or @path/to/file │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
..."
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-expanded-match 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ (r:) Type your message or @path/to/file │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
llllllllllllllllllllllllllllllllllllllllllllllllll"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ (r:) Type your message or @path/to/file │
|
||||
@@ -38,12 +20,6 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ > commit │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 2`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ (r:) commit │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
git commit -m "feat: add search" in src/app"
|
||||
@@ -51,12 +27,6 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ > commit │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 2`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ (r:) commit │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
git commit -m "feat: add search" in src/app"
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import type React from 'react';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { type MutableRefObject } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
+18
-6
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import {
|
||||
vi,
|
||||
describe,
|
||||
@@ -92,9 +93,10 @@ describe('useShellCommandProcessor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const renderProcessorHook = () =>
|
||||
renderHook(() =>
|
||||
useShellCommandProcessor(
|
||||
const renderProcessorHook = () => {
|
||||
let hookResult: ReturnType<typeof useShellCommandProcessor>;
|
||||
function TestComponent() {
|
||||
hookResult = useShellCommandProcessor(
|
||||
addItemToHistoryMock,
|
||||
setPendingHistoryItemMock,
|
||||
onExecMock,
|
||||
@@ -102,8 +104,18 @@ describe('useShellCommandProcessor', () => {
|
||||
mockConfig,
|
||||
mockGeminiClient,
|
||||
setShellInputFocusedMock,
|
||||
),
|
||||
);
|
||||
);
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createMockServiceResult = (
|
||||
overrides: Partial<ShellExecutionResult> = {},
|
||||
+100
-75
@@ -4,8 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
|
||||
import type {
|
||||
CommandContext,
|
||||
@@ -131,8 +132,10 @@ describe('useSlashCommandProcessor', () => {
|
||||
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
|
||||
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSlashCommandProcessor(
|
||||
let hookResult: ReturnType<typeof useSlashCommandProcessor>;
|
||||
|
||||
function TestComponent() {
|
||||
hookResult = useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockAddItem,
|
||||
@@ -159,10 +162,19 @@ describe('useSlashCommandProcessor', () => {
|
||||
},
|
||||
new Map(), // extensionsUpdateState
|
||||
true, // isConfigInitialized
|
||||
),
|
||||
);
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
const { unmount, rerender } = render(<TestComponent />);
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
unmount,
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
};
|
||||
};
|
||||
|
||||
describe('Initialization and Command Loading', () => {
|
||||
@@ -177,7 +189,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
const testCommand = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([testCommand]);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -191,7 +203,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
const testCommand = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([testCommand]);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -219,7 +231,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
const result = setupProcessorHook([builtinCommand], [fileCommand]);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
// The service should only return one command with the name 'override'
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
@@ -237,7 +249,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
describe('Command Execution Logic', () => {
|
||||
it('should display an error for an unknown command', async () => {
|
||||
const result = setupProcessorHook();
|
||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toBeDefined(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/nonexistent');
|
||||
@@ -268,7 +282,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
],
|
||||
};
|
||||
const result = setupProcessorHook([parentCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/parent');
|
||||
@@ -302,7 +318,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
],
|
||||
};
|
||||
const result = setupProcessorHook([parentCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/parent child with args');
|
||||
@@ -348,7 +366,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
setMockIsProcessing,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toBeDefined(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/fail');
|
||||
@@ -366,7 +386,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
const result = setupProcessorHook([command], [], [], mockSetIsProcessing);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
const executionPromise = act(async () => {
|
||||
await result.current.handleSlashCommand('/long-running');
|
||||
@@ -392,7 +414,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
action: vi.fn().mockResolvedValue({ type: 'dialog', dialog: 'theme' }),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/themecmd');
|
||||
@@ -407,7 +431,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
action: vi.fn().mockResolvedValue({ type: 'dialog', dialog: 'model' }),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/modelcmd');
|
||||
@@ -432,7 +458,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
}),
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/load');
|
||||
@@ -468,7 +496,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/loadwiththoughts');
|
||||
@@ -488,7 +518,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit');
|
||||
@@ -510,7 +542,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([], [fileCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
let actionResult;
|
||||
await act(async () => {
|
||||
@@ -542,7 +576,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([], [], [mcpCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
let actionResult;
|
||||
await act(async () => {
|
||||
@@ -584,7 +620,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should set confirmation request when action returns confirm_shell_commands', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.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.
|
||||
@@ -593,7 +631,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
// We now wait for the state to be updated with the request.
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -604,14 +642,16 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should do nothing if user cancels confirmation', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
|
||||
// Wait for the confirmation dialog to be set
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -637,12 +677,14 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should re-run command with one-time allowlist on "Proceed Once"', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -663,7 +705,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
expect(result.current.shellConfirmationRequest).toBeNull();
|
||||
|
||||
// The action should have been called twice (initial + re-run).
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCommandAction).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -691,12 +733,14 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should re-run command and update session allowlist on "Proceed Always"', async () => {
|
||||
const result = setupProcessorHook([shellCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleSlashCommand('/shellcmd');
|
||||
});
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.shellConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -712,7 +756,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
|
||||
expect(result.current.shellConfirmationRequest).toBeNull();
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCommandAction).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -722,7 +766,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
);
|
||||
|
||||
// Check that the session-wide allowlist WAS updated.
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
const finalContext = result.current.commandContext;
|
||||
expect(finalContext.session.sessionShellAllowlist.has('rm -rf /')).toBe(
|
||||
true,
|
||||
@@ -735,7 +779,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
it('should be case-sensitive', async () => {
|
||||
const command = createTestCommand({ name: 'test' });
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
// Use uppercase when command is lowercase
|
||||
@@ -761,7 +807,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
action,
|
||||
});
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/alias');
|
||||
@@ -777,7 +825,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
const action = vi.fn();
|
||||
const command = createTestCommand({ name: 'test', action });
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand(' /test with-args ');
|
||||
@@ -790,7 +840,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
const action = vi.fn();
|
||||
const command = createTestCommand({ name: 'help', action });
|
||||
const result = setupProcessorHook([command]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(1),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('?help');
|
||||
@@ -820,7 +872,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
const result = setupProcessorHook([], [fileCommand], [mcpCommand]);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
// The service should only return one command with the name 'override'
|
||||
expect(result.current.slashCommands).toHaveLength(1);
|
||||
});
|
||||
@@ -856,7 +908,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
// so the test must work regardless of which comes first.
|
||||
const result = setupProcessorHook([quitCommand], [exitCommand]);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.slashCommands).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -882,7 +934,9 @@ describe('useSlashCommandProcessor', () => {
|
||||
);
|
||||
|
||||
const result = setupProcessorHook([quitCommand], [exitCommand]);
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands).toHaveLength(2),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit');
|
||||
@@ -899,36 +953,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
describe('Lifecycle', () => {
|
||||
it('should abort command loading when the hook unmounts', () => {
|
||||
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
|
||||
const { unmount } = renderHook(() =>
|
||||
useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockAddItem,
|
||||
mockClearItems,
|
||||
mockLoadHistory,
|
||||
vi.fn(), // refreshStatic
|
||||
vi.fn().mockResolvedValue(false), // toggleVimEnabled
|
||||
vi.fn(), // setIsProcessing
|
||||
vi.fn(), // setGeminiMdFileCount
|
||||
{
|
||||
openAuthDialog: vi.fn(),
|
||||
openThemeDialog: vi.fn(),
|
||||
openEditorDialog: vi.fn(),
|
||||
openPrivacyNotice: vi.fn(),
|
||||
openSettingsDialog: vi.fn(),
|
||||
openModelDialog: vi.fn(),
|
||||
openPermissionsDialog: vi.fn(),
|
||||
quit: vi.fn(),
|
||||
setDebugMessage: vi.fn(),
|
||||
toggleCorgiMode: vi.fn(),
|
||||
toggleDebugProfiler: vi.fn(),
|
||||
dispatchExtensionStateUpdate: vi.fn(),
|
||||
addConfirmUpdateExtensionRequest: vi.fn(),
|
||||
},
|
||||
new Map(), // extensionsUpdateState
|
||||
true, // isConfigInitialized
|
||||
),
|
||||
);
|
||||
const { unmount } = setupProcessorHook();
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -972,7 +997,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should log a simple slash command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -991,7 +1016,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('logs nothing for a bogus command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1003,7 +1028,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('logs a failure event for a failed command', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1022,7 +1047,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should log a slash command with a subcommand', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1040,7 +1065,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should log the command path when an alias is used', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -1056,7 +1081,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
it('should not log for unknown commands', async () => {
|
||||
const result = setupProcessorHook(loggingTestCommands);
|
||||
await waitFor(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(result.current.slashCommands?.length).toBeGreaterThan(0),
|
||||
);
|
||||
await act(async () => {
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
|
||||
+113
-256
@@ -4,8 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
@@ -15,12 +13,12 @@ import {
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { act, useEffect } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useCommandCompletion } from './useCommandCompletion.js';
|
||||
import type { CommandContext } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { useTextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { useEffect } from 'react';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
import type { UseAtCompletionProps } from './useAtCompletion.js';
|
||||
import { useAtCompletion } from './useAtCompletion.js';
|
||||
@@ -93,7 +91,8 @@ describe('useCommandCompletion', () => {
|
||||
const mockCommandContext = {} as CommandContext;
|
||||
const mockConfig = {
|
||||
getEnablePromptCompletion: () => false,
|
||||
} as Config;
|
||||
getGeminiClient: vi.fn(),
|
||||
} as unknown as Config;
|
||||
const testDirs: string[] = [];
|
||||
const testRootDir = '/';
|
||||
|
||||
@@ -108,6 +107,40 @@ describe('useCommandCompletion', () => {
|
||||
});
|
||||
}
|
||||
|
||||
const renderCommandCompletionHook = (
|
||||
initialText: string,
|
||||
cursorOffset?: number,
|
||||
shellModeActive = false,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useCommandCompletion> & {
|
||||
textBuffer: ReturnType<typeof useTextBuffer>;
|
||||
};
|
||||
|
||||
function TestComponent() {
|
||||
const textBuffer = useTextBufferForTest(initialText, cursorOffset);
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
shellModeActive,
|
||||
mockConfig,
|
||||
);
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset to default mocks before each test
|
||||
@@ -121,18 +154,7 @@ describe('useCommandCompletion', () => {
|
||||
describe('Core Hook Behavior', () => {
|
||||
describe('State Management', () => {
|
||||
it('should initialize with default state', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(''),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('');
|
||||
|
||||
expect(result.current.suggestions).toEqual([]);
|
||||
expect(result.current.activeSuggestionIndex).toBe(-1);
|
||||
@@ -146,26 +168,13 @@ describe('useCommandCompletion', () => {
|
||||
atSuggestions: [{ label: 'src/file.txt', value: 'src/file.txt' }],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('@file');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { completion, textBuffer };
|
||||
const { result } = renderCommandCompletionHook('@file');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.completion.suggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(result.current.completion.showSuggestions).toBe(true);
|
||||
expect(result.current.showSuggestions).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.textBuffer.replaceRangeByOffset(
|
||||
@@ -175,24 +184,13 @@ describe('useCommandCompletion', () => {
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.completion.showSuggestions).toBe(false);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.showSuggestions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset all state to default values', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('@files'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('@files');
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveSuggestionIndex(5);
|
||||
@@ -210,20 +208,9 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
it('should call useAtCompletion with the correct query for an escaped space', async () => {
|
||||
const text = '@src/a\\ file.txt';
|
||||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
renderCommandCompletionHook(text);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(useAtCompletion).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
@@ -237,20 +224,9 @@ describe('useCommandCompletion', () => {
|
||||
const text = '@file1 @file2';
|
||||
const cursorOffset = 3; // @fi|le1 @file2
|
||||
|
||||
renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest(text, cursorOffset),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
renderCommandCompletionHook(text, cursorOffset);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(useAtCompletion).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
@@ -286,22 +262,13 @@ describe('useCommandCompletion', () => {
|
||||
slashSuggestions: [{ label: 'clear', value: 'clear' }],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('/');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
shellModeActive, // Parameterized shellModeActive
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook(
|
||||
'/',
|
||||
undefined,
|
||||
shellModeActive,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(expectedSuggestions);
|
||||
expect(result.current.showSuggestions).toBe(
|
||||
expectedShowSuggestions,
|
||||
@@ -327,18 +294,7 @@ describe('useCommandCompletion', () => {
|
||||
it('should handle navigateUp with no suggestions', () => {
|
||||
setupMocks({ slashSuggestions: [] });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
act(() => {
|
||||
result.current.navigateUp();
|
||||
@@ -349,18 +305,7 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
it('should handle navigateDown with no suggestions', () => {
|
||||
setupMocks({ slashSuggestions: [] });
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
act(() => {
|
||||
result.current.navigateDown();
|
||||
@@ -370,20 +315,9 @@ describe('useCommandCompletion', () => {
|
||||
});
|
||||
|
||||
it('should navigate up through suggestions with wrap-around', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -397,20 +331,9 @@ describe('useCommandCompletion', () => {
|
||||
});
|
||||
|
||||
it('should navigate down through suggestions with wrap-around', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -427,20 +350,9 @@ describe('useCommandCompletion', () => {
|
||||
});
|
||||
|
||||
it('should handle navigation with multiple suggestions', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -465,20 +377,9 @@ describe('useCommandCompletion', () => {
|
||||
it('should automatically select the first item when suggestions are available', async () => {
|
||||
setupMocks({ slashSuggestions: mockSuggestions });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommandCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
const { result } = renderCommandCompletionHook('/');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(
|
||||
mockSuggestions.length,
|
||||
);
|
||||
@@ -495,22 +396,9 @@ describe('useCommandCompletion', () => {
|
||||
slashCompletionRange: { completionStart: 1, completionEnd: 4 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('/mem');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook('/mem');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -526,22 +414,9 @@ describe('useCommandCompletion', () => {
|
||||
atSuggestions: [{ label: 'src/file1.txt', value: 'src/file1.txt' }],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('@src/fi');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook('@src/fi');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -560,22 +435,9 @@ describe('useCommandCompletion', () => {
|
||||
atSuggestions: [{ label: 'src/file1.txt', value: 'src/file1.txt' }],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest(text, cursorOffset);
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook(text, cursorOffset);
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -593,22 +455,9 @@ describe('useCommandCompletion', () => {
|
||||
atSuggestions: [{ label: 'src/components/', value: 'src/components/' }],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('@src/comp');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook('@src/comp');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -626,22 +475,9 @@ describe('useCommandCompletion', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
const textBuffer = useTextBufferForTest('@src\\comp');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
false,
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
const { result } = renderCommandCompletionHook('@src\\comp');
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -657,9 +493,14 @@ describe('useCommandCompletion', () => {
|
||||
it('should not trigger prompt completion for line comments', async () => {
|
||||
const mockConfig = {
|
||||
getEnablePromptCompletion: () => true,
|
||||
} as Config;
|
||||
getGeminiClient: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
let hookResult: ReturnType<typeof useCommandCompletion> & {
|
||||
textBuffer: ReturnType<typeof useTextBuffer>;
|
||||
};
|
||||
|
||||
function TestComponent() {
|
||||
const textBuffer = useTextBufferForTest('// This is a line comment');
|
||||
const completion = useCommandCompletion(
|
||||
textBuffer,
|
||||
@@ -671,19 +512,26 @@ describe('useCommandCompletion', () => {
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
|
||||
// Should not trigger prompt completion for comments
|
||||
expect(result.current.suggestions.length).toBe(0);
|
||||
expect(hookResult!.suggestions.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not trigger prompt completion for block comments', async () => {
|
||||
const mockConfig = {
|
||||
getEnablePromptCompletion: () => true,
|
||||
} as Config;
|
||||
getGeminiClient: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
let hookResult: ReturnType<typeof useCommandCompletion> & {
|
||||
textBuffer: ReturnType<typeof useTextBuffer>;
|
||||
};
|
||||
|
||||
function TestComponent() {
|
||||
const textBuffer = useTextBufferForTest(
|
||||
'/* This is a block comment */',
|
||||
);
|
||||
@@ -697,19 +545,26 @@ describe('useCommandCompletion', () => {
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
|
||||
// Should not trigger prompt completion for comments
|
||||
expect(result.current.suggestions.length).toBe(0);
|
||||
expect(hookResult!.suggestions.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should trigger prompt completion for regular text when enabled', async () => {
|
||||
const mockConfig = {
|
||||
getEnablePromptCompletion: () => true,
|
||||
} as Config;
|
||||
getGeminiClient: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
let hookResult: ReturnType<typeof useCommandCompletion> & {
|
||||
textBuffer: ReturnType<typeof useTextBuffer>;
|
||||
};
|
||||
|
||||
function TestComponent() {
|
||||
const textBuffer = useTextBufferForTest(
|
||||
'This is regular text that should trigger completion',
|
||||
);
|
||||
@@ -723,11 +578,13 @@ describe('useCommandCompletion', () => {
|
||||
false,
|
||||
mockConfig,
|
||||
);
|
||||
return { ...completion, textBuffer };
|
||||
});
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
|
||||
// This test verifies that comments are filtered out while regular text is not
|
||||
expect(result.current.textBuffer.text).toBe(
|
||||
expect(hookResult!.textBuffer.text).toBe(
|
||||
'This is regular text that should trigger completion',
|
||||
);
|
||||
});
|
||||
+26
-9
@@ -4,10 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { act, useCallback } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { useConsoleMessages } from './useConsoleMessages.js';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
describe('useConsoleMessages', () => {
|
||||
beforeEach(() => {
|
||||
@@ -38,13 +38,30 @@ describe('useConsoleMessages', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const renderConsoleMessagesHook = () => {
|
||||
let hookResult: ReturnType<typeof useTestableConsoleMessages>;
|
||||
function TestComponent() {
|
||||
hookResult = useTestableConsoleMessages();
|
||||
return null;
|
||||
}
|
||||
const { unmount } = render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with an empty array of console messages', () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
expect(result.current.consoleMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add a new message when log is called', async () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
|
||||
act(() => {
|
||||
result.current.log('Test message');
|
||||
@@ -60,7 +77,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
it('should batch and count identical consecutive messages', async () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
|
||||
act(() => {
|
||||
result.current.log('Test message');
|
||||
@@ -78,7 +95,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
it('should not batch different messages', async () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
|
||||
act(() => {
|
||||
result.current.log('First message');
|
||||
@@ -96,7 +113,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
it('should clear all messages when clearConsoleMessages is called', async () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
|
||||
act(() => {
|
||||
result.current.log('A message');
|
||||
@@ -116,7 +133,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
it('should clear the pending timeout when clearConsoleMessages is called', () => {
|
||||
const { result } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result } = renderConsoleMessagesHook();
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
|
||||
act(() => {
|
||||
@@ -132,7 +149,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
it('should clean up the timeout on unmount', () => {
|
||||
const { result, unmount } = renderHook(() => useTestableConsoleMessages());
|
||||
const { result, unmount } = renderConsoleMessagesHook();
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
|
||||
act(() => {
|
||||
+44
-54
@@ -14,7 +14,7 @@ import {
|
||||
type MockedFunction,
|
||||
} from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useEditorSettings } from './useEditorSettings.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
@@ -43,6 +43,16 @@ describe('useEditorSettings', () => {
|
||||
let mockAddItem: MockedFunction<
|
||||
(item: Omit<HistoryItem, 'id'>, timestamp: number) => void
|
||||
>;
|
||||
let result: ReturnType<typeof useEditorSettings>;
|
||||
|
||||
function TestComponent() {
|
||||
result = useEditorSettings(
|
||||
mockLoadedSettings,
|
||||
mockSetEditorError,
|
||||
mockAddItem,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
@@ -64,47 +74,39 @@ describe('useEditorSettings', () => {
|
||||
});
|
||||
|
||||
it('should initialize with dialog closed', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(result.current.isEditorDialogOpen).toBe(false);
|
||||
expect(result.isEditorDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should open editor dialog when openEditorDialog is called', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.openEditorDialog();
|
||||
});
|
||||
|
||||
expect(result.current.isEditorDialogOpen).toBe(true);
|
||||
expect(result.isEditorDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should close editor dialog when exitEditorDialog is called', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.exitEditorDialog();
|
||||
result.openEditorDialog();
|
||||
result.exitEditorDialog();
|
||||
});
|
||||
expect(result.current.isEditorDialogOpen).toBe(false);
|
||||
expect(result.isEditorDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle editor selection successfully', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
const editorType: EditorType = 'vscode';
|
||||
const scope = SettingScope.User;
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.openEditorDialog();
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
|
||||
@@ -122,19 +124,17 @@ describe('useEditorSettings', () => {
|
||||
);
|
||||
|
||||
expect(mockSetEditorError).toHaveBeenCalledWith(null);
|
||||
expect(result.current.isEditorDialogOpen).toBe(false);
|
||||
expect(result.isEditorDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle clearing editor preference (undefined editor)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
const scope = SettingScope.Workspace;
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.handleEditorSelect(undefined, scope);
|
||||
result.openEditorDialog();
|
||||
result.handleEditorSelect(undefined, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
|
||||
@@ -152,20 +152,18 @@ describe('useEditorSettings', () => {
|
||||
);
|
||||
|
||||
expect(mockSetEditorError).toHaveBeenCalledWith(null);
|
||||
expect(result.current.isEditorDialogOpen).toBe(false);
|
||||
expect(result.isEditorDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle different editor types', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
const editorTypes: EditorType[] = ['cursor', 'windsurf', 'vim'];
|
||||
const scope = SettingScope.User;
|
||||
|
||||
editorTypes.forEach((editorType) => {
|
||||
act(() => {
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
|
||||
@@ -185,16 +183,14 @@ describe('useEditorSettings', () => {
|
||||
});
|
||||
|
||||
it('should handle different setting scopes', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
const editorType: EditorType = 'vscode';
|
||||
const scopes = [SettingScope.User, SettingScope.Workspace];
|
||||
|
||||
scopes.forEach((scope) => {
|
||||
act(() => {
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
|
||||
@@ -214,9 +210,7 @@ describe('useEditorSettings', () => {
|
||||
});
|
||||
|
||||
it('should not set preference for unavailable editors', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
mockCheckHasEditorType.mockReturnValue(false);
|
||||
|
||||
@@ -224,19 +218,17 @@ describe('useEditorSettings', () => {
|
||||
const scope = SettingScope.User;
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.openEditorDialog();
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).not.toHaveBeenCalled();
|
||||
expect(mockAddItem).not.toHaveBeenCalled();
|
||||
expect(result.current.isEditorDialogOpen).toBe(true);
|
||||
expect(result.isEditorDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should not set preference for editors not allowed in sandbox', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
mockAllowEditorTypeInSandbox.mockReturnValue(false);
|
||||
|
||||
@@ -244,19 +236,17 @@ describe('useEditorSettings', () => {
|
||||
const scope = SettingScope.User;
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.openEditorDialog();
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockLoadedSettings.setValue).not.toHaveBeenCalled();
|
||||
expect(mockAddItem).not.toHaveBeenCalled();
|
||||
expect(result.current.isEditorDialogOpen).toBe(true);
|
||||
expect(result.isEditorDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle errors during editor selection', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEditorSettings(mockLoadedSettings, mockSetEditorError, mockAddItem),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
const errorMessage = 'Failed to save settings';
|
||||
(
|
||||
@@ -271,14 +261,14 @@ describe('useEditorSettings', () => {
|
||||
const scope = SettingScope.User;
|
||||
|
||||
act(() => {
|
||||
result.current.openEditorDialog();
|
||||
result.current.handleEditorSelect(editorType, scope);
|
||||
result.openEditorDialog();
|
||||
result.handleEditorSelect(editorType, scope);
|
||||
});
|
||||
|
||||
expect(mockSetEditorError).toHaveBeenCalledWith(
|
||||
`Failed to set editor preference: Error: ${errorMessage}`,
|
||||
);
|
||||
expect(mockAddItem).not.toHaveBeenCalled();
|
||||
expect(result.current.isEditorDialogOpen).toBe(true);
|
||||
expect(result.isEditorDialogOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
+30
-18
@@ -11,7 +11,7 @@ import * as path from 'node:path';
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
import { useExtensionUpdates } from './useExtensionUpdates.js';
|
||||
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
@@ -25,7 +25,7 @@ vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...mockedOs,
|
||||
homedir: vi.fn(),
|
||||
homedir: vi.fn().mockReturnValue('/tmp/mock-home'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -96,15 +96,18 @@ describe('useExtensionUpdates', () => {
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() =>
|
||||
function TestComponent() {
|
||||
useExtensionUpdates(
|
||||
extensions as GeminiCLIExtension[],
|
||||
extensionManager,
|
||||
addItem,
|
||||
),
|
||||
);
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
@@ -148,11 +151,14 @@ describe('useExtensionUpdates', () => {
|
||||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates([extension], extensionManager, addItem),
|
||||
);
|
||||
function TestComponent() {
|
||||
useExtensionUpdates([extension], extensionManager, addItem);
|
||||
return null;
|
||||
}
|
||||
|
||||
await waitFor(
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -226,11 +232,14 @@ describe('useExtensionUpdates', () => {
|
||||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions, extensionManager, addItem),
|
||||
);
|
||||
function TestComponent() {
|
||||
useExtensionUpdates(extensions, extensionManager, addItem);
|
||||
return null;
|
||||
}
|
||||
|
||||
await waitFor(
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(2);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
@@ -308,15 +317,18 @@ describe('useExtensionUpdates', () => {
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() =>
|
||||
function TestComponent() {
|
||||
useExtensionUpdates(
|
||||
extensions as GeminiCLIExtension[],
|
||||
extensionManager,
|
||||
addItem,
|
||||
),
|
||||
);
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
render(<TestComponent />);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import { useFlickerDetector } from './useFlickerDetector.js';
|
||||
|
||||
+29
-11
@@ -4,13 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { useFocus } from './useFocus.js';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import { useStdin, useStdout } from 'ink';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import React from 'react';
|
||||
import { act } from 'react';
|
||||
|
||||
// Mock the ink hooks
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
@@ -25,9 +25,6 @@ vi.mock('ink', async (importOriginal) => {
|
||||
const mockedUseStdin = vi.mocked(useStdin);
|
||||
const mockedUseStdout = vi.mocked(useStdout);
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(KeypressProvider, null, children);
|
||||
|
||||
describe('useFocus', () => {
|
||||
let stdin: EventEmitter & { resume: Mock; pause: Mock };
|
||||
let stdout: { write: Mock };
|
||||
@@ -51,15 +48,36 @@ describe('useFocus', () => {
|
||||
stdin.removeAllListeners();
|
||||
});
|
||||
|
||||
const renderFocusHook = () => {
|
||||
let hookResult: ReturnType<typeof useFocus>;
|
||||
function TestComponent() {
|
||||
hookResult = useFocus();
|
||||
return null;
|
||||
}
|
||||
const { unmount } = render(
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<TestComponent />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with focus and enable focus reporting', () => {
|
||||
const { result } = renderHook(() => useFocus(), { wrapper });
|
||||
const { result } = renderFocusHook();
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
expect(stdout.write).toHaveBeenCalledWith('\x1b[?1004h');
|
||||
});
|
||||
|
||||
it('should set isFocused to false when a focus-out event is received', () => {
|
||||
const { result } = renderHook(() => useFocus(), { wrapper });
|
||||
const { result } = renderFocusHook();
|
||||
|
||||
// Initial state is focused
|
||||
expect(result.current).toBe(true);
|
||||
@@ -74,7 +92,7 @@ describe('useFocus', () => {
|
||||
});
|
||||
|
||||
it('should set isFocused to true when a focus-in event is received', () => {
|
||||
const { result } = renderHook(() => useFocus(), { wrapper });
|
||||
const { result } = renderFocusHook();
|
||||
|
||||
// Simulate focus-out to set initial state to false
|
||||
act(() => {
|
||||
@@ -92,7 +110,7 @@ describe('useFocus', () => {
|
||||
});
|
||||
|
||||
it('should clean up and disable focus reporting on unmount', () => {
|
||||
const { unmount } = renderHook(() => useFocus(), { wrapper });
|
||||
const { unmount } = renderFocusHook();
|
||||
|
||||
// At this point we should have listeners from both KeypressProvider and useFocus
|
||||
const listenerCountAfterMount = stdin.listenerCount('data');
|
||||
@@ -107,7 +125,7 @@ describe('useFocus', () => {
|
||||
});
|
||||
|
||||
it('should handle multiple focus events correctly', () => {
|
||||
const { result } = renderHook(() => useFocus(), { wrapper });
|
||||
const { result } = renderFocusHook();
|
||||
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from('\x1b[O'));
|
||||
@@ -131,7 +149,7 @@ describe('useFocus', () => {
|
||||
});
|
||||
|
||||
it('restores focus on keypress after focus is lost', () => {
|
||||
const { result } = renderHook(() => useFocus(), { wrapper });
|
||||
const { result } = renderFocusHook();
|
||||
|
||||
// Simulate focus-out event
|
||||
act(() => {
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { vi, type Mock, type MockInstance } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useFolderTrust } from './useFolderTrust.js';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Mock, MockInstance } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
+29
-11
@@ -7,7 +7,7 @@
|
||||
import type { MockedFunction } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useGitBranchName } from './useGitBranchName.js';
|
||||
import { fs, vol } from 'memfs';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
@@ -54,13 +54,31 @@ describe('useGitBranchName', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderGitBranchNameHook = (cwd: string) => {
|
||||
let hookResult: ReturnType<typeof useGitBranchName>;
|
||||
function TestComponent() {
|
||||
hookResult = useGitBranchName(cwd);
|
||||
return null;
|
||||
}
|
||||
const { rerender, unmount } = render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
it('should return branch name', async () => {
|
||||
(mockSpawnAsync as MockedFunction<typeof mockSpawnAsync>).mockResolvedValue(
|
||||
{
|
||||
stdout: 'main\n',
|
||||
} as { stdout: string; stderr: string },
|
||||
);
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
|
||||
await act(async () => {
|
||||
rerender(); // Rerender to get the updated state
|
||||
@@ -74,7 +92,7 @@ describe('useGitBranchName', () => {
|
||||
new Error('Git error'),
|
||||
);
|
||||
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
@@ -95,7 +113,7 @@ describe('useGitBranchName', () => {
|
||||
return { stdout: '' } as { stdout: string; stderr: string };
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
@@ -114,7 +132,7 @@ describe('useGitBranchName', () => {
|
||||
return { stdout: '' } as { stdout: string; stderr: string };
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
@@ -135,7 +153,7 @@ describe('useGitBranchName', () => {
|
||||
stderr: string;
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
|
||||
await act(async () => {
|
||||
rerender();
|
||||
@@ -143,7 +161,7 @@ describe('useGitBranchName', () => {
|
||||
expect(result.current).toBe('main');
|
||||
|
||||
// Wait for watcher to be set up
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(watchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -153,7 +171,7 @@ describe('useGitBranchName', () => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current).toBe('develop');
|
||||
});
|
||||
});
|
||||
@@ -168,7 +186,7 @@ describe('useGitBranchName', () => {
|
||||
} as { stdout: string; stderr: string },
|
||||
);
|
||||
|
||||
const { result, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { result, rerender } = renderGitBranchNameHook(CWD);
|
||||
|
||||
await act(async () => {
|
||||
rerender();
|
||||
@@ -211,14 +229,14 @@ describe('useGitBranchName', () => {
|
||||
} as { stdout: string; stderr: string },
|
||||
);
|
||||
|
||||
const { unmount, rerender } = renderHook(() => useGitBranchName(CWD));
|
||||
const { unmount, rerender } = renderGitBranchNameHook(CWD);
|
||||
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
// Wait for watcher to be set up BEFORE unmounting
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(watchMock).toHaveBeenCalledWith(
|
||||
GIT_LOGS_HEAD_PATH,
|
||||
expect.any(Function),
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useHistory } from './useHistoryManager.js';
|
||||
|
||||
+24
-8
@@ -4,9 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { act } from 'react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
IdeClient,
|
||||
@@ -79,13 +78,30 @@ describe('useIdeTrustListener', () => {
|
||||
);
|
||||
});
|
||||
|
||||
const renderTrustListenerHook = () => {
|
||||
let hookResult: ReturnType<typeof useIdeTrustListener>;
|
||||
function TestComponent() {
|
||||
hookResult = useIdeTrustListener();
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: () => rerender(<TestComponent />),
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize correctly with no trust information', () => {
|
||||
vi.mocked(trustedFolders.isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: undefined,
|
||||
source: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIdeTrustListener());
|
||||
const { result } = renderTrustListenerHook();
|
||||
|
||||
expect(result.current.isIdeTrusted).toBe(undefined);
|
||||
expect(result.current.needsRestart).toBe(false);
|
||||
@@ -100,7 +116,7 @@ describe('useIdeTrustListener', () => {
|
||||
isTrusted: true,
|
||||
source: 'ide',
|
||||
});
|
||||
const { result } = renderHook(() => useIdeTrustListener());
|
||||
const { result } = renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -134,7 +150,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIdeTrustListener());
|
||||
const { result } = renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -172,7 +188,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIdeTrustListener());
|
||||
const { result } = renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -208,7 +224,7 @@ describe('useIdeTrustListener', () => {
|
||||
source: 'ide',
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() => useIdeTrustListener());
|
||||
const { result, rerender } = renderTrustListenerHook();
|
||||
|
||||
// Manually trigger the initial connection state for the test setup
|
||||
await act(async () => {
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useInputHistory } from './useInputHistory.js';
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useInputHistoryStore } from './useInputHistoryStore.js';
|
||||
|
||||
+25
-39
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { useStdin } from 'ink';
|
||||
@@ -44,8 +44,17 @@ describe('useKeypress', () => {
|
||||
const onKeypress = vi.fn();
|
||||
let originalNodeVersion: string;
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(KeypressProvider, null, children);
|
||||
const renderKeypressHook = (isActive = true) => {
|
||||
function TestComponent() {
|
||||
useKeypress(onKeypress, { isActive });
|
||||
return null;
|
||||
}
|
||||
return render(
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<TestComponent />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -67,9 +76,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should not listen if isActive is false', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: false }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(false);
|
||||
act(() => stdin.write('a'));
|
||||
expect(onKeypress).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -81,33 +88,27 @@ describe('useKeypress', () => {
|
||||
{ key: { name: 'up', sequence: '\x1b[A' } },
|
||||
{ key: { name: 'down', sequence: '\x1b[B' } },
|
||||
])('should listen for keypress when active for key $key.name', ({ key }) => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||
renderKeypressHook(true);
|
||||
act(() => stdin.write(key.sequence));
|
||||
expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
|
||||
});
|
||||
|
||||
it('should set and release raw mode', () => {
|
||||
const { unmount } = renderHook(
|
||||
() => useKeypress(onKeypress, { isActive: true }),
|
||||
{ wrapper },
|
||||
);
|
||||
const { unmount } = renderKeypressHook(true);
|
||||
expect(mockSetRawMode).toHaveBeenCalledWith(true);
|
||||
unmount();
|
||||
expect(mockSetRawMode).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should stop listening after being unmounted', () => {
|
||||
const { unmount } = renderHook(
|
||||
() => useKeypress(onKeypress, { isActive: true }),
|
||||
{ wrapper },
|
||||
);
|
||||
const { unmount } = renderKeypressHook(true);
|
||||
unmount();
|
||||
act(() => stdin.write('a'));
|
||||
expect(onKeypress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly identify alt+enter (meta key)', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||
renderKeypressHook(true);
|
||||
const key = { name: 'return', sequence: '\x1B\r' };
|
||||
act(() => stdin.write(key.sequence));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
@@ -130,9 +131,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should process a paste as a single event', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
const pasteText = 'hello world';
|
||||
act(() => stdin.write(PASTE_START + pasteText + PASTE_END));
|
||||
|
||||
@@ -148,9 +147,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should handle keypress interspersed with pastes', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
|
||||
const keyA = { name: 'a', sequence: 'a' };
|
||||
act(() => stdin.write('a'));
|
||||
@@ -174,9 +171,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should handle lone pastes', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
|
||||
const pasteText = 'pasted';
|
||||
act(() => {
|
||||
@@ -192,9 +187,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should handle paste false alarm', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
|
||||
act(() => {
|
||||
stdin.write(PASTE_START.slice(0, 5));
|
||||
@@ -211,9 +204,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should handle back to back pastes', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
|
||||
const pasteText1 = 'herp';
|
||||
const pasteText2 = 'derp';
|
||||
@@ -238,9 +229,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should handle pastes split across writes', async () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||
wrapper,
|
||||
});
|
||||
renderKeypressHook(true);
|
||||
|
||||
const keyA = { name: 'a', sequence: 'a' };
|
||||
act(() => stdin.write('a'));
|
||||
@@ -272,10 +261,7 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
it('should emit partial paste content if unmounted mid-paste', () => {
|
||||
const { unmount } = renderHook(
|
||||
() => useKeypress(onKeypress, { isActive: true }),
|
||||
{ wrapper },
|
||||
);
|
||||
const { unmount } = renderKeypressHook(true);
|
||||
const pasteText = 'incomplete paste';
|
||||
|
||||
act(() => stdin.write(PASTE_START + pasteText));
|
||||
+36
-16
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useLoadingIndicator } from './useLoadingIndicator.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import {
|
||||
@@ -24,11 +25,35 @@ describe('useLoadingIndicator', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderLoadingIndicatorHook = (
|
||||
initialStreamingState: StreamingState,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||
function TestComponent({
|
||||
streamingState,
|
||||
}: {
|
||||
streamingState: StreamingState;
|
||||
}) {
|
||||
hookResult = useLoadingIndicator(streamingState);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(
|
||||
<TestComponent streamingState={initialStreamingState} />,
|
||||
);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: { streamingState: StreamingState }) =>
|
||||
rerender(<TestComponent {...newProps} />),
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with default values when Idle', () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result } = renderHook(() =>
|
||||
useLoadingIndicator(StreamingState.Idle),
|
||||
);
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Idle);
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
expect(WITTY_LOADING_PHRASES).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
@@ -37,9 +62,7 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
it('should reflect values when Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result } = renderHook(() =>
|
||||
useLoadingIndicator(StreamingState.Responding),
|
||||
);
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Responding);
|
||||
|
||||
// Initial state before timers advance
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
@@ -58,9 +81,8 @@ describe('useLoadingIndicator', () => {
|
||||
});
|
||||
|
||||
it('should show waiting phrase and retain elapsedTime when WaitingForConfirmation', async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) => useLoadingIndicator(streamingState),
|
||||
{ initialProps: { streamingState: StreamingState.Responding } },
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
@@ -86,9 +108,8 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) => useLoadingIndicator(streamingState),
|
||||
{ initialProps: { streamingState: StreamingState.Responding } },
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
@@ -120,9 +141,8 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
it('should reset timer and phrase when streamingState changes from Responding to Idle', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) => useLoadingIndicator(streamingState),
|
||||
{ initialProps: { streamingState: StreamingState.Responding } },
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
+10
-5
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { vi } from 'vitest';
|
||||
import {
|
||||
useMemoryMonitor,
|
||||
@@ -27,11 +27,16 @@ describe('useMemoryMonitor', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function TestComponent() {
|
||||
useMemoryMonitor({ addItem });
|
||||
return null;
|
||||
}
|
||||
|
||||
it('should not warn when memory usage is below threshold', () => {
|
||||
memoryUsageSpy.mockReturnValue({
|
||||
rss: MEMORY_WARNING_THRESHOLD / 2,
|
||||
} as NodeJS.MemoryUsage);
|
||||
renderHook(() => useMemoryMonitor({ addItem }));
|
||||
render(<TestComponent />);
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -40,7 +45,7 @@ describe('useMemoryMonitor', () => {
|
||||
memoryUsageSpy.mockReturnValue({
|
||||
rss: MEMORY_WARNING_THRESHOLD * 1.5,
|
||||
} as NodeJS.MemoryUsage);
|
||||
renderHook(() => useMemoryMonitor({ addItem }));
|
||||
render(<TestComponent />);
|
||||
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
@@ -56,7 +61,7 @@ describe('useMemoryMonitor', () => {
|
||||
memoryUsageSpy.mockReturnValue({
|
||||
rss: MEMORY_WARNING_THRESHOLD * 1.5,
|
||||
} as NodeJS.MemoryUsage);
|
||||
const { rerender } = renderHook(() => useMemoryMonitor({ addItem }));
|
||||
const { rerender } = render(<TestComponent />);
|
||||
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -64,7 +69,7 @@ describe('useMemoryMonitor', () => {
|
||||
memoryUsageSpy.mockReturnValue({
|
||||
rss: MEMORY_WARNING_THRESHOLD * 1.5,
|
||||
} as NodeJS.MemoryUsage);
|
||||
rerender();
|
||||
rerender(<TestComponent />);
|
||||
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
+108
-123
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useMessageQueue } from './useMessageQueue.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
|
||||
@@ -22,27 +23,45 @@ describe('useMessageQueue', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderMessageQueueHook = (initialProps: {
|
||||
isConfigInitialized: boolean;
|
||||
streamingState: StreamingState;
|
||||
submitQuery: (query: string) => void;
|
||||
}) => {
|
||||
let hookResult: ReturnType<typeof useMessageQueue>;
|
||||
function TestComponent(props: typeof initialProps) {
|
||||
hookResult = useMessageQueue(props);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(<TestComponent {...initialProps} />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: Partial<typeof initialProps>) =>
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />),
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with empty queue', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
expect(result.current.getQueuedMessagesText()).toBe('');
|
||||
});
|
||||
|
||||
it('should add messages to queue', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Test message 1');
|
||||
@@ -56,13 +75,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should filter out empty messages', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Valid message');
|
||||
@@ -78,13 +95,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should clear queue', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Test message');
|
||||
@@ -100,13 +115,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should return queued messages as text with double newlines', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Message 1');
|
||||
@@ -119,18 +132,12 @@ describe('useMessageQueue', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-submit queued messages when transitioning to Idle', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
{
|
||||
initialProps: { streamingState: StreamingState.Responding },
|
||||
},
|
||||
);
|
||||
it('should auto-submit queued messages when transitioning to Idle', async () => {
|
||||
const { result, rerender } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Add some messages
|
||||
act(() => {
|
||||
@@ -143,22 +150,18 @@ describe('useMessageQueue', () => {
|
||||
// Transition to Idle
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Message 1\n\nMessage 2');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
await vi.waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Message 1\n\nMessage 2');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not auto-submit when queue is empty', () => {
|
||||
const { rerender } = renderHook(
|
||||
({ streamingState }) =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
{
|
||||
initialProps: { streamingState: StreamingState.Responding },
|
||||
},
|
||||
);
|
||||
const { rerender } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Transition to Idle with empty queue
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
@@ -167,17 +170,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should not auto-submit when not transitioning to Idle', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
{
|
||||
initialProps: { streamingState: StreamingState.Responding },
|
||||
},
|
||||
);
|
||||
const { result, rerender } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Add messages
|
||||
act(() => {
|
||||
@@ -191,18 +188,12 @@ describe('useMessageQueue', () => {
|
||||
expect(result.current.messageQueue).toEqual(['Message 1']);
|
||||
});
|
||||
|
||||
it('should handle multiple state transitions correctly', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ streamingState }) =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
{
|
||||
initialProps: { streamingState: StreamingState.Idle },
|
||||
},
|
||||
);
|
||||
it('should handle multiple state transitions correctly', async () => {
|
||||
const { result, rerender } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Start responding
|
||||
rerender({ streamingState: StreamingState.Responding });
|
||||
@@ -215,8 +206,10 @@ describe('useMessageQueue', () => {
|
||||
// Go back to idle - should submit
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('First batch');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
await vi.waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('First batch');
|
||||
expect(result.current.messageQueue).toEqual([]);
|
||||
});
|
||||
|
||||
// Start responding again
|
||||
rerender({ streamingState: StreamingState.Responding });
|
||||
@@ -229,19 +222,19 @@ describe('useMessageQueue', () => {
|
||||
// Go back to idle - should submit again
|
||||
rerender({ streamingState: StreamingState.Idle });
|
||||
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Second batch');
|
||||
expect(mockSubmitQuery).toHaveBeenCalledTimes(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(mockSubmitQuery).toHaveBeenCalledWith('Second batch');
|
||||
expect(mockSubmitQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popAllMessages', () => {
|
||||
it('should pop all messages and return them joined with double newlines', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Add multiple messages
|
||||
act(() => {
|
||||
@@ -269,13 +262,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should return undefined when queue is empty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
let poppedMessages: string | undefined = 'not-undefined';
|
||||
act(() => {
|
||||
@@ -289,13 +280,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should handle single message correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Single message');
|
||||
@@ -313,13 +302,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should clear the entire queue after popping', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addMessage('Message 1');
|
||||
@@ -346,13 +333,11 @@ describe('useMessageQueue', () => {
|
||||
});
|
||||
|
||||
it('should work correctly with state updates', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMessageQueue({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
}),
|
||||
);
|
||||
const { result } = renderMessageQueueHook({
|
||||
isConfigInitialized: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
submitQuery: mockSubmitQuery,
|
||||
});
|
||||
|
||||
// Add messages
|
||||
act(() => {
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useModelCommand } from './useModelCommand.js';
|
||||
|
||||
describe('useModelCommand', () => {
|
||||
it('should initialize with the model dialog closed', () => {
|
||||
const { result } = renderHook(() => useModelCommand());
|
||||
expect(result.current.isModelDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should open the model dialog when openModelDialog is called', () => {
|
||||
const { result } = renderHook(() => useModelCommand());
|
||||
|
||||
act(() => {
|
||||
result.current.openModelDialog();
|
||||
});
|
||||
|
||||
expect(result.current.isModelDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should close the model dialog when closeModelDialog is called', () => {
|
||||
const { result } = renderHook(() => useModelCommand());
|
||||
|
||||
// Open it first
|
||||
act(() => {
|
||||
result.current.openModelDialog();
|
||||
});
|
||||
expect(result.current.isModelDialogOpen).toBe(true);
|
||||
|
||||
// Then close it
|
||||
act(() => {
|
||||
result.current.closeModelDialog();
|
||||
});
|
||||
expect(result.current.isModelDialogOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useModelCommand } from './useModelCommand.js';
|
||||
|
||||
describe('useModelCommand', () => {
|
||||
let result: ReturnType<typeof useModelCommand>;
|
||||
|
||||
function TestComponent() {
|
||||
result = useModelCommand();
|
||||
return null;
|
||||
}
|
||||
|
||||
it('should initialize with the model dialog closed', () => {
|
||||
render(<TestComponent />);
|
||||
expect(result.isModelDialogOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should open the model dialog when openModelDialog is called', () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
act(() => {
|
||||
result.openModelDialog();
|
||||
});
|
||||
|
||||
expect(result.isModelDialogOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should close the model dialog when closeModelDialog is called', () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
// Open it first
|
||||
act(() => {
|
||||
result.openModelDialog();
|
||||
});
|
||||
expect(result.isModelDialogOpen).toBe(true);
|
||||
|
||||
// Then close it
|
||||
act(() => {
|
||||
result.closeModelDialog();
|
||||
});
|
||||
expect(result.isModelDialogOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
import {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import {
|
||||
|
||||
+26
-10
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import type {
|
||||
Config,
|
||||
CodeAssistServer,
|
||||
@@ -31,12 +31,28 @@ describe('usePrivacySettings', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderPrivacySettingsHook = () => {
|
||||
let hookResult: ReturnType<typeof usePrivacySettings>;
|
||||
function TestComponent() {
|
||||
hookResult = usePrivacySettings(mockConfig);
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('should throw error when content generator is not a CodeAssistServer', async () => {
|
||||
vi.mocked(getCodeAssistServer).mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => usePrivacySettings(mockConfig));
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -53,9 +69,9 @@ describe('usePrivacySettings', () => {
|
||||
}) as unknown as LoadCodeAssistResponse,
|
||||
} as unknown as CodeAssistServer);
|
||||
|
||||
const { result } = renderHook(() => usePrivacySettings(mockConfig));
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -72,9 +88,9 @@ describe('usePrivacySettings', () => {
|
||||
}) as unknown as LoadCodeAssistResponse,
|
||||
} as unknown as CodeAssistServer);
|
||||
|
||||
const { result } = renderHook(() => usePrivacySettings(mockConfig));
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -99,10 +115,10 @@ describe('usePrivacySettings', () => {
|
||||
} as unknown as CodeAssistServer;
|
||||
vi.mocked(getCodeAssistServer).mockReturnValue(mockCodeAssistServer);
|
||||
|
||||
const { result } = renderHook(() => usePrivacySettings(mockConfig));
|
||||
const { result } = renderPrivacySettingsHook();
|
||||
|
||||
// Wait for initial load
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.privacyState.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -110,7 +126,7 @@ describe('usePrivacySettings', () => {
|
||||
await result.current.updateDataCollectionOptIn(false);
|
||||
|
||||
// Wait for update to complete
|
||||
await waitFor(() => {
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.privacyState.dataCollectionOptIn).toBe(false);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import {
|
||||
vi,
|
||||
describe,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { CoreToolScheduler } from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
+303
-348
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import {
|
||||
useSelectionList,
|
||||
type SelectionListItem,
|
||||
@@ -66,40 +67,64 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const renderSelectionListHook = (initialProps: {
|
||||
items: Array<SelectionListItem<string>>;
|
||||
onSelect: (item: string) => void;
|
||||
onHighlight?: (item: string) => void;
|
||||
initialIndex?: number;
|
||||
isFocused?: boolean;
|
||||
showNumbers?: boolean;
|
||||
}) => {
|
||||
let hookResult: ReturnType<typeof useSelectionList>;
|
||||
function TestComponent(props: typeof initialProps) {
|
||||
hookResult = useSelectionList(props);
|
||||
return null;
|
||||
}
|
||||
const { rerender, unmount } = render(<TestComponent {...initialProps} />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: Partial<typeof initialProps>) =>
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />),
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should initialize with the default index (0) if enabled', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize with the provided initialIndex if enabled', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle an empty list gracefully', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items: [], onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: [],
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should find the next enabled item (downwards) if initialIndex is disabled', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
@@ -109,33 +134,27 @@ describe('useSelectionList', () => {
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', disabled: true, key: 'C' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: wrappingItems,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: wrappingItems,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should default to 0 if initialIndex is out of bounds', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: 10,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 10,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
|
||||
const { result: resultNeg } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: -1,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result: resultNeg } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: -1,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(resultNeg.current.activeIndex).toBe(0);
|
||||
});
|
||||
|
||||
@@ -144,22 +163,21 @@ describe('useSelectionList', () => {
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: allDisabled,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: allDisabled,
|
||||
initialIndex: 1,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard Navigation (Up/Down/J/K)', () => {
|
||||
it('should move down with "j" and "down" keys, skipping disabled items', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
pressKey('j');
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
@@ -168,9 +186,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should move up with "k" and "up" keys, skipping disabled items', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items, initialIndex: 3, onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 3,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
pressKey('k');
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
@@ -179,13 +199,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should wrap navigation correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: items.length - 1,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: items.length - 1,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
@@ -195,13 +213,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should call onHighlight when index changes', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
}),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
});
|
||||
pressKey('down');
|
||||
expect(mockOnHighlight).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnHighlight).toHaveBeenCalledWith('C');
|
||||
@@ -209,13 +225,11 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', () => {
|
||||
const singleItem = [{ value: 'A', key: 'A' }];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: singleItem,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: singleItem,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
});
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
expect(mockOnHighlight).not.toHaveBeenCalled();
|
||||
@@ -226,13 +240,11 @@ describe('useSelectionList', () => {
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: allDisabled,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: allDisabled,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
});
|
||||
const initialIndex = result.current.activeIndex;
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(initialIndex);
|
||||
@@ -242,25 +254,21 @@ describe('useSelectionList', () => {
|
||||
|
||||
describe('Selection (Enter)', () => {
|
||||
it('should call onSelect when "return" is pressed on enabled item', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items,
|
||||
initialIndex: 2,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
pressKey('return');
|
||||
expect(mockOnSelect).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('C');
|
||||
});
|
||||
|
||||
it('should not call onSelect if the active item is disabled', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
|
||||
act(() => result.current.setActiveIndex(1));
|
||||
|
||||
@@ -271,13 +279,11 @@ describe('useSelectionList', () => {
|
||||
|
||||
describe('Keyboard Navigation Robustness (Rapid Input)', () => {
|
||||
it('should handle rapid navigation and selection robustly (avoiding stale state)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
});
|
||||
|
||||
// Simulate rapid inputs with separate act blocks to allow effects to run
|
||||
if (!activeKeypressHandler) throw new Error('Handler not active');
|
||||
@@ -321,13 +327,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should handle ultra-rapid input (multiple presses in single act) without stale state', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items, // A, B(disabled), C, D. Initial index 0 (A).
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
});
|
||||
|
||||
// Simulate ultra-rapid inputs where all keypresses happen faster than React can re-render
|
||||
act(() => {
|
||||
@@ -363,40 +367,41 @@ describe('useSelectionList', () => {
|
||||
|
||||
describe('Focus Management (isFocused)', () => {
|
||||
it('should activate the keypress handler when focused (default) and items exist', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
expect(activeKeypressHandler).not.toBeNull();
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should not activate the keypress handler when isFocused is false', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect, isFocused: false }),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: false,
|
||||
});
|
||||
expect(activeKeypressHandler).toBeNull();
|
||||
expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
|
||||
});
|
||||
|
||||
it('should not activate the keypress handler when items list is empty', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({
|
||||
items: [],
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: true,
|
||||
}),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items: [],
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: true,
|
||||
});
|
||||
expect(activeKeypressHandler).toBeNull();
|
||||
expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
|
||||
});
|
||||
|
||||
it('should activate/deactivate when isFocused prop changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { isFocused: boolean }) =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect, ...props }),
|
||||
{ initialProps: { isFocused: false } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
isFocused: false,
|
||||
});
|
||||
|
||||
expect(activeKeypressHandler).toBeNull();
|
||||
|
||||
@@ -429,23 +434,22 @@ describe('useSelectionList', () => {
|
||||
const pressNumber = (num: string) => pressKey(num, num);
|
||||
|
||||
it('should not respond to numbers if showNumbers is false (default)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items: shortList, onSelect: mockOnSelect }),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
pressNumber('1');
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should select item immediately if the number cannot be extended (unambiguous)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
});
|
||||
pressNumber('3');
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
@@ -456,15 +460,13 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should highlight and wait for timeout if the number can be extended (ambiguous)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
initialIndex: 1, // Start at index 1 so pressing "1" (index 0) causes a change
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: longList,
|
||||
initialIndex: 1, // Start at index 1 so pressing "1" (index 0) causes a change
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('1');
|
||||
|
||||
@@ -483,13 +485,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should handle multi-digit input correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('1');
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
@@ -503,13 +503,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should reset buffer if input becomes invalid (out of bounds)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: shortList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('5');
|
||||
|
||||
@@ -522,13 +520,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should allow "0" as subsequent digit, but ignore as first digit', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('0');
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
@@ -545,13 +541,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should clear the initial "0" input after timeout', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('0');
|
||||
act(() => vi.advanceTimersByTime(1000)); // Timeout the '0' input
|
||||
@@ -564,14 +558,12 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should highlight but not select a disabled item (immediate selection case)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: shortList, // B (index 1, number 2) is disabled
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: shortList, // B (index 1, number 2) is disabled
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('2');
|
||||
|
||||
@@ -589,13 +581,11 @@ describe('useSelectionList', () => {
|
||||
...longList.slice(1),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: disabledAmbiguousList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: disabledAmbiguousList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('1');
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
@@ -610,13 +600,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should clear the number buffer if a non-numeric key (e.g., navigation) is pressed', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { result } = renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('1');
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
@@ -632,13 +620,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should clear the number buffer if "return" is pressed', () => {
|
||||
renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressNumber('1');
|
||||
|
||||
@@ -655,31 +641,25 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
describe('Reactivity (Dynamic Updates)', () => {
|
||||
it('should update activeIndex when initialIndex prop changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ initialIndex }: { initialIndex: number }) =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex,
|
||||
}),
|
||||
{ initialProps: { initialIndex: 0 } },
|
||||
);
|
||||
it('should update activeIndex when initialIndex prop changes', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
rerender({ initialIndex: 2 });
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a new initialIndex even after user interaction', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ initialIndex }: { initialIndex: number }) =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex,
|
||||
}),
|
||||
{ initialProps: { initialIndex: 0 } },
|
||||
);
|
||||
it('should respect a new initialIndex even after user interaction', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
// User navigates, changing the active index
|
||||
pressKey('down');
|
||||
@@ -689,35 +669,31 @@ describe('useSelectionList', () => {
|
||||
rerender({ initialIndex: 3 });
|
||||
|
||||
// The hook should now respect the new initial index
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate index when initialIndex prop changes to a disabled item', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ initialIndex }: { initialIndex: number }) =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex,
|
||||
}),
|
||||
{ initialProps: { initialIndex: 0 } },
|
||||
);
|
||||
it('should validate index when initialIndex prop changes to a disabled item', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
rerender({ initialIndex: 1 });
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should adjust activeIndex if items change and the initialIndex is now out of bounds', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 3,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items } },
|
||||
);
|
||||
it('should adjust activeIndex if items change and the initialIndex is now out of bounds', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 3,
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
|
||||
@@ -728,24 +704,22 @@ describe('useSelectionList', () => {
|
||||
rerender({ items: shorterItems }); // Length 2
|
||||
|
||||
// The useEffect syncs based on the initialIndex (3) which is now out of bounds. It defaults to 0.
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should adjust activeIndex if items change and the initialIndex becomes disabled', () => {
|
||||
it('should adjust activeIndex if items change and the initialIndex becomes disabled', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
|
||||
@@ -756,25 +730,25 @@ describe('useSelectionList', () => {
|
||||
];
|
||||
rerender({ items: newItems });
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset to 0 if items change to an empty list', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 2,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items } },
|
||||
);
|
||||
it('should reset to 0 if items change to an empty list', async () => {
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 2,
|
||||
items,
|
||||
});
|
||||
|
||||
rerender({ items: [] });
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not reset activeIndex when items are deeply equal', () => {
|
||||
it('should not reset activeIndex when items are deeply equal', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
@@ -782,16 +756,12 @@ describe('useSelectionList', () => {
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 2,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 2,
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
|
||||
@@ -813,12 +783,14 @@ describe('useSelectionList', () => {
|
||||
rerender({ items: newItems });
|
||||
|
||||
// Active index should remain the same since items are deeply equal
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
// onHighlight should NOT be called since the index didn't change
|
||||
expect(mockOnHighlight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update activeIndex when items change structurally', () => {
|
||||
it('should update activeIndex when items change structurally', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
@@ -826,16 +798,12 @@ describe('useSelectionList', () => {
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 3,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
initialIndex: 3,
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
mockOnHighlight.mockClear();
|
||||
@@ -850,25 +818,23 @@ describe('useSelectionList', () => {
|
||||
rerender({ items: newItems });
|
||||
|
||||
// Active index should update based on initialIndex and new items
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle partial changes in items array', () => {
|
||||
it('should handle partial changes in items array', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex: 1,
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
|
||||
@@ -882,24 +848,22 @@ describe('useSelectionList', () => {
|
||||
rerender({ items: newItems });
|
||||
|
||||
// Should find next valid index since current became disabled
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update selection when a new item is added to the start of the list', () => {
|
||||
it('should update selection when a new item is added to the start of the list', async () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
const { result, rerender } = renderSelectionListHook({
|
||||
onSelect: mockOnSelect,
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
@@ -913,7 +877,9 @@ describe('useSelectionList', () => {
|
||||
|
||||
rerender({ items: newItems });
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not re-initialize when items have identical keys but are different objects', () => {
|
||||
@@ -924,17 +890,26 @@ describe('useSelectionList', () => {
|
||||
|
||||
let renderCount = 0;
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) => {
|
||||
const renderHookWithCount = (initialProps: {
|
||||
items: Array<SelectionListItem<string>>;
|
||||
}) => {
|
||||
function TestComponent(props: typeof initialProps) {
|
||||
renderCount++;
|
||||
return useSelectionList({
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
items: testItems,
|
||||
items: props.items,
|
||||
});
|
||||
},
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(<TestComponent {...initialProps} />);
|
||||
return {
|
||||
rerender: (newProps: Partial<typeof initialProps>) =>
|
||||
rerender(<TestComponent {...initialProps} {...newProps} />),
|
||||
};
|
||||
};
|
||||
|
||||
const { rerender } = renderHookWithCount({ items: initialItems });
|
||||
|
||||
// Initial render
|
||||
expect(renderCount).toBe(1);
|
||||
@@ -950,24 +925,6 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manual Control', () => {
|
||||
it('should allow manual setting of active index via setActiveIndex', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({ items, onSelect: mockOnSelect }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveIndex(3);
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveIndex(1);
|
||||
});
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cleanup', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -983,13 +940,11 @@ describe('useSelectionList', () => {
|
||||
(_, i) => ({ value: `Item ${i + 1}`, key: `Item ${i + 1}` }),
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
}),
|
||||
);
|
||||
const { unmount } = renderSelectionListHook({
|
||||
items: longList,
|
||||
onSelect: mockOnSelect,
|
||||
showNumbers: true,
|
||||
});
|
||||
|
||||
pressKey('1', '1');
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useShellHistory } from './useShellHistory.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
|
||||
+55
-26
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useTimer } from './useTimer.js';
|
||||
|
||||
describe('useTimer', () => {
|
||||
@@ -17,13 +18,43 @@ describe('useTimer', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderTimerHook = (
|
||||
initialIsActive: boolean,
|
||||
initialResetKey: number,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useTimer>;
|
||||
function TestComponent({
|
||||
isActive,
|
||||
resetKey,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
resetKey: number;
|
||||
}) {
|
||||
hookResult = useTimer(isActive, resetKey);
|
||||
return null;
|
||||
}
|
||||
const { rerender, unmount } = render(
|
||||
<TestComponent isActive={initialIsActive} resetKey={initialResetKey} />,
|
||||
);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: { isActive: boolean; resetKey: number }) =>
|
||||
rerender(<TestComponent {...newProps} />),
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with 0', () => {
|
||||
const { result } = renderHook(() => useTimer(false, 0));
|
||||
const { result } = renderTimerHook(false, 0);
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('should not increment time if isActive is false', () => {
|
||||
const { result } = renderHook(() => useTimer(false, 0));
|
||||
const { result } = renderTimerHook(false, 0);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000);
|
||||
});
|
||||
@@ -31,7 +62,7 @@ describe('useTimer', () => {
|
||||
});
|
||||
|
||||
it('should increment time every second if isActive is true', () => {
|
||||
const { result } = renderHook(() => useTimer(true, 0));
|
||||
const { result } = renderTimerHook(true, 0);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
@@ -43,13 +74,12 @@ describe('useTimer', () => {
|
||||
});
|
||||
|
||||
it('should reset to 0 and start incrementing when isActive becomes true from false', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isActive, resetKey }) => useTimer(isActive, resetKey),
|
||||
{ initialProps: { isActive: false, resetKey: 0 } },
|
||||
);
|
||||
const { result, rerender } = renderTimerHook(false, 0);
|
||||
expect(result.current).toBe(0);
|
||||
|
||||
rerender({ isActive: true, resetKey: 0 });
|
||||
act(() => {
|
||||
rerender({ isActive: true, resetKey: 0 });
|
||||
});
|
||||
expect(result.current).toBe(0); // Should reset to 0 upon becoming active
|
||||
|
||||
act(() => {
|
||||
@@ -59,16 +89,15 @@ describe('useTimer', () => {
|
||||
});
|
||||
|
||||
it('should reset to 0 when resetKey changes while active', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isActive, resetKey }) => useTimer(isActive, resetKey),
|
||||
{ initialProps: { isActive: true, resetKey: 0 } },
|
||||
);
|
||||
const { result, rerender } = renderTimerHook(true, 0);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3000); // 3s
|
||||
});
|
||||
expect(result.current).toBe(3);
|
||||
|
||||
rerender({ isActive: true, resetKey: 1 }); // Change resetKey
|
||||
act(() => {
|
||||
rerender({ isActive: true, resetKey: 1 }); // Change resetKey
|
||||
});
|
||||
expect(result.current).toBe(0); // Should reset to 0
|
||||
|
||||
act(() => {
|
||||
@@ -78,39 +107,39 @@ describe('useTimer', () => {
|
||||
});
|
||||
|
||||
it('should be 0 if isActive is false, regardless of resetKey changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isActive, resetKey }) => useTimer(isActive, resetKey),
|
||||
{ initialProps: { isActive: false, resetKey: 0 } },
|
||||
);
|
||||
const { result, rerender } = renderTimerHook(false, 0);
|
||||
expect(result.current).toBe(0);
|
||||
|
||||
rerender({ isActive: false, resetKey: 1 });
|
||||
act(() => {
|
||||
rerender({ isActive: false, resetKey: 1 });
|
||||
});
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('should clear timer on unmount', () => {
|
||||
const { unmount } = renderHook(() => useTimer(true, 0));
|
||||
const { unmount } = renderTimerHook(true, 0);
|
||||
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
||||
unmount();
|
||||
expect(clearIntervalSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should preserve elapsedTime when isActive becomes false, and reset to 0 when it becomes active again', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isActive, resetKey }) => useTimer(isActive, resetKey),
|
||||
{ initialProps: { isActive: true, resetKey: 0 } },
|
||||
);
|
||||
const { result, rerender } = renderTimerHook(true, 0);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3000); // Advance to 3 seconds
|
||||
});
|
||||
expect(result.current).toBe(3);
|
||||
|
||||
rerender({ isActive: false, resetKey: 0 });
|
||||
act(() => {
|
||||
rerender({ isActive: false, resetKey: 0 });
|
||||
});
|
||||
expect(result.current).toBe(3); // Time should be preserved when timer becomes inactive
|
||||
|
||||
// Now make it active again, it should reset to 0
|
||||
rerender({ isActive: true, resetKey: 0 });
|
||||
act(() => {
|
||||
rerender({ isActive: true, resetKey: 0 });
|
||||
});
|
||||
expect(result.current).toBe(0);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { useVim } from './vim.js';
|
||||
import type { VimMode } from './vim.js';
|
||||
import type { Key } from './useKeypress.js';
|
||||
@@ -173,10 +174,25 @@ describe('useVim hook', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const renderVimHook = (buffer?: Partial<TextBuffer>) =>
|
||||
renderHook(() =>
|
||||
useVim((buffer || mockBuffer) as TextBuffer, mockHandleFinalSubmit),
|
||||
);
|
||||
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 exitInsertMode = (result: {
|
||||
current: {
|
||||
@@ -1286,10 +1302,14 @@ describe('useVim hook', () => {
|
||||
});
|
||||
|
||||
describe('Shell command pass-through', () => {
|
||||
it('should pass through ctrl+r in INSERT mode', () => {
|
||||
it('should pass through ctrl+r in INSERT mode', async () => {
|
||||
mockVimContext.vimMode = 'INSERT';
|
||||
const { result } = renderVimHook();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
const handled = result.current.handleInput(
|
||||
createKey({ name: 'r', ctrl: true }),
|
||||
);
|
||||
@@ -1297,20 +1317,29 @@ describe('useVim hook', () => {
|
||||
expect(handled).toBe(false);
|
||||
});
|
||||
|
||||
it('should pass through ! in INSERT mode when buffer is empty', () => {
|
||||
it('should pass through ! in INSERT mode when buffer is empty', async () => {
|
||||
mockVimContext.vimMode = 'INSERT';
|
||||
const emptyBuffer = createMockBuffer('');
|
||||
const { result } = renderVimHook(emptyBuffer);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
const handled = result.current.handleInput(createKey({ sequence: '!' }));
|
||||
|
||||
expect(handled).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ! as input in INSERT mode when buffer is not empty', () => {
|
||||
it('should handle ! as input in INSERT mode when buffer is not empty', async () => {
|
||||
mockVimContext.vimMode = 'INSERT';
|
||||
const nonEmptyBuffer = createMockBuffer('not empty');
|
||||
const { result } = renderVimHook(nonEmptyBuffer);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(result.current.mode).toBe('INSERT');
|
||||
});
|
||||
|
||||
const key = createKey({ sequence: '!', name: '!' });
|
||||
|
||||
act(() => {
|
||||
@@ -6,18 +6,25 @@
|
||||
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', 'config.test.ts'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**'],
|
||||
environment: 'jsdom',
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
reporters: ['default', 'junit'],
|
||||
silent: true,
|
||||
outputFile: {
|
||||
junit: 'junit.xml',
|
||||
},
|
||||
alias: {
|
||||
react: path.resolve(__dirname, '../../node_modules/react'),
|
||||
},
|
||||
setupFiles: ['./test-setup.ts'],
|
||||
coverage: {
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user