Migrate tests to use avoid jsdom (#12118)

This commit is contained in:
Jacob Richman
2025-10-28 10:32:15 -07:00
committed by GitHub
parent 5d61adf804
commit 13aa0148e7
31 changed files with 765 additions and 579 deletions
@@ -4,10 +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 { act } from 'react';
import { vi } from 'vitest';
import { FolderTrustDialog } from './FolderTrustDialog.js';
import * as processUtils from '../../utils/processUtils.js';
@@ -56,12 +54,12 @@ describe('FolderTrustDialog', () => {
stdin.write('\u001b[27u'); // Press kitty escape key
});
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain(
'A folder trust level must be selected to continue. Exiting since escape was pressed.',
);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockedExit).toHaveBeenCalledWith(1);
});
expect(onSelect).not.toHaveBeenCalled();
@@ -95,7 +93,7 @@ describe('FolderTrustDialog', () => {
stdin.write('r');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockedExit).not.toHaveBeenCalled();
});
});
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { render } from 'ink-testing-library';
import { describe, it, expect } from 'vitest';
import { Help } from './Help.js';
@@ -4,9 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { render, cleanup } from '@testing-library/react';
import { render, cleanup } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -82,12 +80,12 @@ describe('<ModelDialog />', () => {
});
it('renders the title and help text', () => {
const { getByText } = renderComponent();
expect(getByText('Select Model')).toBeDefined();
expect(getByText('(Press Esc to close)')).toBeDefined();
expect(
getByText('> To use a specific Gemini model, use the --model flag.'),
).toBeDefined();
const { lastFrame } = renderComponent();
expect(lastFrame()).toContain('Select Model');
expect(lastFrame()).toContain('(Press Esc to close)');
expect(lastFrame()).toContain(
'> To use a specific Gemini model, use the --model flag.',
);
});
it('passes all model options to DescriptiveRadioButtonSelect', () => {
@@ -4,16 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
/// <reference types="vitest/globals" />
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
import { TrustLevel } from '../../config/trustedFolders.js';
import { waitFor, act } from '@testing-library/react';
import { act } from 'react';
import * as processUtils from '../../utils/processUtils.js';
import { usePermissionsModifyTrust } from '../hooks/usePermissionsModifyTrust.js';
@@ -72,7 +68,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Modify Trust Level');
expect(lastFrame()).toContain('Folder: /test/dir');
expect(lastFrame()).toContain('Current Level: DO_NOT_TRUST');
@@ -94,7 +90,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain(
'Note: This folder behaves as a trusted folder because one of the parent folders is trusted.',
);
@@ -116,7 +112,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain(
'Note: This folder behaves as a trusted folder because the connected IDE workspace is trusted.',
);
@@ -128,7 +124,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Trust this folder (dir)');
expect(lastFrame()).toContain('Trust parent folder (test)');
});
@@ -140,13 +136,13 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => {
stdin.write('\u001b[27u'); // Kitty escape key
});
await waitFor(() => {
await vi.waitFor(() => {
expect(onExit).toHaveBeenCalled();
});
});
@@ -171,11 +167,11 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => stdin.write('r')); // Press 'r' to restart
await waitFor(() => {
await vi.waitFor(() => {
expect(mockCommitTrustLevelChange).toHaveBeenCalled();
expect(mockRelaunchApp).toHaveBeenCalled();
expect(onExit).toHaveBeenCalled();
@@ -201,11 +197,11 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => stdin.write('\u001b[27u')); // Press kitty escape key
await waitFor(() => {
await vi.waitFor(() => {
expect(mockCommitTrustLevelChange).not.toHaveBeenCalled();
expect(onExit).toHaveBeenCalled();
});
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
/**
*
*
@@ -30,7 +28,6 @@ import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { VimModeProvider } from '../contexts/VimModeContext.js';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { waitFor } from '@testing-library/react';
import { saveModifiedSettings, TEST_ONLY } from '../../utils/settingsUtils.js';
import {
getSettingsSchema,
@@ -408,7 +405,7 @@ describe('SettingsDialog', () => {
const { stdin, unmount, lastFrame } = render(component);
// Wait for initial render and verify we're on Vim Mode (first setting)
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('● Vim Mode');
});
@@ -416,7 +413,7 @@ describe('SettingsDialog', () => {
act(() => {
stdin.write(TerminalKeys.DOWN_ARROW as string);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('● Disable Auto Update');
});
@@ -425,14 +422,14 @@ describe('SettingsDialog', () => {
stdin.write(TerminalKeys.ENTER as string);
});
// Wait for the setting change to be processed
await waitFor(() => {
await vi.waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
});
// Wait for the mock to be called
await waitFor(() => {
await vi.waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
@@ -470,7 +467,7 @@ describe('SettingsDialog', () => {
await wait();
stdin.write(TerminalKeys.ENTER as string);
await wait();
await waitFor(() => {
await vi.waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
@@ -507,7 +504,7 @@ describe('SettingsDialog', () => {
await wait();
stdin.write(TerminalKeys.ENTER as string);
await wait();
await waitFor(() => {
await vi.waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
@@ -596,7 +593,7 @@ describe('SettingsDialog', () => {
);
// Wait for initial render
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -668,7 +665,7 @@ describe('SettingsDialog', () => {
);
// Wait for initial render
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Hide Window Title');
});
@@ -964,7 +961,7 @@ describe('SettingsDialog', () => {
await wait(50);
}
await waitFor(() => {
await vi.waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
@@ -1024,7 +1021,7 @@ describe('SettingsDialog', () => {
await wait(30);
}
await waitFor(() => {
await vi.waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
@@ -1141,7 +1138,7 @@ describe('SettingsDialog', () => {
);
// Wait for initial render
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -1203,7 +1200,7 @@ describe('SettingsDialog', () => {
);
// Wait for initial render
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -4,10 +4,7 @@
* 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';
import {
BaseSelectionList,
@@ -301,7 +298,7 @@ describe('BaseSelectionList', () => {
rerender(<BaseSelectionList {...componentProps} />);
await waitFor(() => {
await vi.waitFor(() => {
expect(lastFrame()).toBeTruthy();
});
};
@@ -325,7 +322,7 @@ describe('BaseSelectionList', () => {
// New visible window should be Items 2, 3, 4 (scroll offset 1).
await updateActiveIndex(3);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).not.toContain('Item 1');
expect(output).toContain('Item 2');
@@ -339,7 +336,7 @@ describe('BaseSelectionList', () => {
await updateActiveIndex(4);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
expect(output).toContain('Item 5');
@@ -350,7 +347,7 @@ describe('BaseSelectionList', () => {
// This should trigger scroll up to show items 2, 3, 4
await updateActiveIndex(1);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
@@ -364,7 +361,7 @@ describe('BaseSelectionList', () => {
// Visible items: 8, 9, 10.
const { lastFrame } = renderScrollableList(9);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 10');
expect(output).toContain('Item 8');
@@ -383,14 +380,14 @@ describe('BaseSelectionList', () => {
expect(lastFrame()).toContain('Item 1');
await updateActiveIndex(3); // Should trigger scroll
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 1');
});
await updateActiveIndex(5); // Scroll further
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 4');
expect(output).toContain('Item 6');
@@ -417,7 +414,7 @@ describe('BaseSelectionList', () => {
it('should correctly identify the selected item when scrolled (high index)', async () => {
renderScrollableList(5);
await waitFor(() => {
await vi.waitFor(() => {
// Item 6 (index 5) should be selected
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ value: 'Item 6' }),
@@ -475,7 +472,7 @@ describe('BaseSelectionList', () => {
0,
);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
// At the top, should show first 3 items
expect(output).toContain('Item 1');
@@ -493,7 +490,7 @@ describe('BaseSelectionList', () => {
5,
);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
// After scrolling to middle, should see items around index 5
expect(output).toContain('Item 4');
@@ -512,7 +509,7 @@ describe('BaseSelectionList', () => {
9,
);
await waitFor(() => {
await vi.waitFor(() => {
const output = lastFrame();
// At the end, should show last 3 items
expect(output).toContain('Item 8');
@@ -4,11 +4,10 @@
* 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';
import { act } from 'react';
import { renderHook } from '../../../test-utils/render.js';
import type {
Viewport,
TextBuffer,
@@ -4,10 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import type React from 'react';
import { renderHook, act, waitFor } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import type { Mock } from 'vitest';
import { vi } from 'vitest';
import type { Key } from './KeypressContext.js';
@@ -370,7 +369,7 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.write(PASTE_END);
});
await waitFor(() => {
await vi.waitFor(() => {
// Expect the handler to be called exactly once for the entire paste
expect(keyHandler).toHaveBeenCalledTimes(1);
});
@@ -399,7 +398,7 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.write(PASTE_END);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(keyHandler).toHaveBeenCalledTimes(1);
});
@@ -427,7 +426,7 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.write(PASTE_END.slice(3));
});
await waitFor(() => {
await vi.waitFor(() => {
expect(keyHandler).toHaveBeenCalledTimes(1);
});
@@ -1193,7 +1192,7 @@ describe('Kitty Sequence Parsing', () => {
}
// Should parse once complete
await waitFor(() => {
await vi.waitFor(() => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'escape',
@@ -4,17 +4,40 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { type MutableRefObject } from 'react';
import { type MutableRefObject, Component, type ReactNode } from 'react';
import { render } from 'ink-testing-library';
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { act } from 'react';
import type { SessionMetrics } from './SessionContext.js';
import { SessionStatsProvider, useSessionStats } from './SessionContext.js';
import { describe, it, expect, vi } from 'vitest';
import { uiTelemetryService } from '@google/gemini-cli-core';
class ErrorBoundary extends Component<
{ children: ReactNode; onError: (error: Error) => void },
{ hasError: boolean }
> {
constructor(props: { children: ReactNode; onError: (error: Error) => void }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(_error: Error) {
return { hasError: true };
}
override componentDidCatch(error: Error) {
this.props.onError(error);
}
override render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
/**
* A test harness component that uses the hook and exposes the context value
* via a mutable ref. This allows us to interact with the context's functions
@@ -208,16 +231,22 @@ describe('SessionStatsContext', () => {
});
it('should throw an error when useSessionStats is used outside of a provider', () => {
// Suppress console.error for this test since we expect an error
const onError = vi.fn();
// Suppress console.error from React for this test
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
// Expect renderHook itself to throw when the hook is used outside a provider
expect(() => {
renderHook(() => useSessionStats());
}).toThrow('useSessionStats must be used within a SessionStatsProvider');
} finally {
consoleSpy.mockRestore();
}
render(
<ErrorBoundary onError={onError}>
<TestHarness contextRef={{ current: undefined }} />
</ErrorBoundary>,
);
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
message: 'useSessionStats must be used within a SessionStatsProvider',
}),
);
consoleSpy.mockRestore();
});
});
@@ -4,16 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { act, useState } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useAtCompletion } from './useAtCompletion.js';
import type { Config, FileSearch } from '@google/gemini-cli-core';
import { FileSearchFactory } from '@google/gemini-cli-core';
import type { FileSystemStructure } from '@google/gemini-cli-test-utils';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
import { useState } from 'react';
import type { Suggestion } from '../components/SuggestionsDisplay.js';
// Test harness to capture the state from the hook's callbacks.
@@ -76,7 +74,7 @@ describe('useAtCompletion', () => {
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -106,7 +104,7 @@ describe('useAtCompletion', () => {
useTestHarnessForAtCompletion(true, 'src/', mockConfig, testRootDir),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -129,7 +127,7 @@ describe('useAtCompletion', () => {
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -166,7 +164,7 @@ describe('useAtCompletion', () => {
);
// The hook should find 'cRaZycAsE.txt' even though the pattern is 'CrAzYCaSe'.
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'cRaZycAsE.txt',
]);
@@ -177,15 +175,29 @@ describe('useAtCompletion', () => {
describe('UI State and Loading Behavior', () => {
it('should be in a loading state during initial file system crawl', async () => {
testRootDir = await createTmpDir({});
// Mock FileSearch to be slow to catch the loading state
const mockFileSearch = {
initialize: vi.fn().mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
}),
search: vi.fn().mockResolvedValue([]),
};
vi.spyOn(FileSearchFactory, 'create').mockReturnValue(
mockFileSearch as unknown as FileSearch,
);
const { result } = renderHook(() =>
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
);
// It's initially true because the effect runs synchronously.
expect(result.current.isLoadingSuggestions).toBe(true);
await vi.waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(true);
});
// Wait for the loading to complete.
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
});
@@ -200,7 +212,7 @@ describe('useAtCompletion', () => {
{ initialProps: { pattern: 'a' } },
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'a.txt',
]);
@@ -210,7 +222,7 @@ describe('useAtCompletion', () => {
rerender({ pattern: 'b' });
// Wait for the final result
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'b.txt',
]);
@@ -253,7 +265,7 @@ describe('useAtCompletion', () => {
);
// Wait for the initial search to complete (using real timers)
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'a.txt',
]);
@@ -283,7 +295,7 @@ describe('useAtCompletion', () => {
vi.useRealTimers();
// Wait for the search results to be processed
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'b.txt',
]);
@@ -314,7 +326,7 @@ describe('useAtCompletion', () => {
);
// Wait for the hook to be ready (initialization is complete)
await waitFor(() => {
await vi.waitFor(() => {
expect(mockFileSearch.search).toHaveBeenCalledWith(
'a',
expect.any(Object),
@@ -330,7 +342,7 @@ describe('useAtCompletion', () => {
expect(abortSpy).toHaveBeenCalledTimes(1);
// Wait for the final result, which should be from the second, faster search.
await waitFor(
await vi.waitFor(
() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual(['b']);
},
@@ -357,7 +369,7 @@ describe('useAtCompletion', () => {
);
// Wait for the hook to be ready and have suggestions
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'a.txt',
]);
@@ -389,7 +401,7 @@ describe('useAtCompletion', () => {
);
// Wait for the hook to enter the error state
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
expect(result.current.suggestions).toEqual([]); // No suggestions on error
@@ -420,7 +432,7 @@ describe('useAtCompletion', () => {
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -441,7 +453,7 @@ describe('useAtCompletion', () => {
useTestHarnessForAtCompletion(true, '', undefined, testRootDir),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -469,7 +481,7 @@ describe('useAtCompletion', () => {
);
// Wait for initial suggestions from the first directory
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'file1.txt',
]);
@@ -481,13 +493,13 @@ describe('useAtCompletion', () => {
});
// After CWD changes, suggestions should be cleared and it should load again.
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(true);
expect(result.current.suggestions).toEqual([]);
});
// Wait for the new suggestions from the second directory
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'file2.txt',
]);
@@ -525,7 +537,7 @@ describe('useAtCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBeGreaterThan(0);
});
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import {
describe,
it,
@@ -15,7 +13,8 @@ import {
type MockedFunction,
type Mock,
} from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
import { Config, ApprovalMode } from '@google/gemini-cli-core';
@@ -4,9 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { renderHook } from '@testing-library/react';
import { renderHook } from '../../test-utils/render.js';
import { vi, type Mock } from 'vitest';
import { useFlickerDetector } from './useFlickerDetector.js';
import { useConfig } from '../contexts/ConfigContext.js';
@@ -19,10 +17,15 @@ import { appEvents, AppEvent } from '../../utils/events.js';
// Mock dependencies
vi.mock('../contexts/ConfigContext.js');
vi.mock('../contexts/UIStateContext.js');
vi.mock('@google/gemini-cli-core', () => ({
recordFlickerFrame: vi.fn(),
GEMINI_DIR: '.gemini',
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
recordFlickerFrame: vi.fn(),
GEMINI_DIR: '.gemini',
};
});
vi.mock('ink', async (importOriginal) => {
const original = await importOriginal<typeof import('ink')>();
return {
@@ -4,10 +4,9 @@
* 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 { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useFolderTrust } from './useFolderTrust.js';
import type { LoadedSettings } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
@@ -30,7 +29,6 @@ vi.mock('node:process', async () => {
describe('useFolderTrust', () => {
let mockSettings: LoadedSettings;
let mockTrustedFolders: LoadedTrustedFolders;
let loadTrustedFoldersSpy: MockInstance;
let isWorkspaceTrustedSpy: MockInstance;
let onTrustChange: (isTrusted: boolean | undefined) => void;
let addItem: Mock;
@@ -51,9 +49,9 @@ describe('useFolderTrust', () => {
setValue: vi.fn(),
} as unknown as LoadedTrustedFolders;
loadTrustedFoldersSpy = vi
.spyOn(trustedFolders, 'loadTrustedFolders')
.mockReturnValue(mockTrustedFolders);
vi.spyOn(trustedFolders, 'loadTrustedFolders').mockReturnValue(
mockTrustedFolders,
);
isWorkspaceTrustedSpy = vi.spyOn(trustedFolders, 'isWorkspaceTrusted');
mockedCwd.mockReturnValue('/test/path');
onTrustChange = vi.fn();
@@ -82,7 +80,7 @@ describe('useFolderTrust', () => {
expect(onTrustChange).toHaveBeenCalledWith(false);
});
it('should open dialog when folder trust is undefined', () => {
it('should open dialog when folder trust is undefined', async () => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: undefined,
source: undefined,
@@ -90,7 +88,9 @@ describe('useFolderTrust', () => {
const { result } = renderHook(() =>
useFolderTrust(mockSettings, onTrustChange, addItem),
);
expect(result.current.isFolderTrustDialogOpen).toBe(true);
await vi.waitFor(() => {
expect(result.current.isFolderTrustDialogOpen).toBe(true);
});
expect(onTrustChange).toHaveBeenCalledWith(undefined);
});
@@ -112,26 +112,41 @@ describe('useFolderTrust', () => {
expect(addItem).not.toHaveBeenCalled();
});
it('should handle TRUST_FOLDER choice', () => {
it('should handle TRUST_FOLDER choice', async () => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: undefined,
source: undefined,
});
(mockTrustedFolders.setValue as Mock).mockImplementation(() => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: true,
source: 'file',
});
});
const { result } = renderHook(() =>
useFolderTrust(mockSettings, onTrustChange, addItem),
);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
await vi.waitFor(() => {
expect(result.current.isTrusted).toBeUndefined();
});
expect(loadTrustedFoldersSpy).toHaveBeenCalled();
expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
'/test/path',
TrustLevel.TRUST_FOLDER,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenLastCalledWith(true);
await act(async () => {
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_FOLDER,
);
});
await vi.waitFor(() => {
expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
'/test/path',
TrustLevel.TRUST_FOLDER,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenLastCalledWith(true);
});
});
it('should handle TRUST_PARENT choice', () => {
@@ -177,7 +192,7 @@ describe('useFolderTrust', () => {
expect(result.current.isFolderTrustDialogOpen).toBe(true);
});
it('should do nothing for default choice', () => {
it('should do nothing for default choice', async () => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: undefined,
source: undefined,
@@ -192,24 +207,40 @@ describe('useFolderTrust', () => {
);
});
expect(mockTrustedFolders.setValue).not.toHaveBeenCalled();
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(result.current.isFolderTrustDialogOpen).toBe(true);
expect(onTrustChange).toHaveBeenCalledWith(undefined);
await vi.waitFor(() => {
expect(mockTrustedFolders.setValue).not.toHaveBeenCalled();
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(result.current.isFolderTrustDialogOpen).toBe(true);
expect(onTrustChange).toHaveBeenCalledWith(undefined);
});
});
it('should set isRestarting to true when trust status changes from false to true', () => {
it('should set isRestarting to true when trust status changes from false to true', async () => {
isWorkspaceTrustedSpy.mockReturnValue({ isTrusted: false, source: 'file' }); // Initially untrusted
(mockTrustedFolders.setValue as Mock).mockImplementation(() => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: true,
source: 'file',
});
});
const { result } = renderHook(() =>
useFolderTrust(mockSettings, onTrustChange, addItem),
);
await vi.waitFor(() => {
expect(result.current.isTrusted).toBe(false);
});
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
});
expect(result.current.isRestarting).toBe(true);
expect(result.current.isFolderTrustDialogOpen).toBe(true); // Dialog should stay open
await vi.waitFor(() => {
expect(result.current.isRestarting).toBe(true);
expect(result.current.isFolderTrustDialogOpen).toBe(true); // Dialog should stay open
});
});
it('should not set isRestarting to true when trust status does not change', () => {
@@ -4,12 +4,11 @@
* 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';
import { renderHook, act, waitFor } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useGeminiStream } from './useGeminiStream.js';
import { useKeypress } from './useKeypress.js';
import * as atCommandProcessor from './atCommandProcessor.js';
@@ -507,7 +506,7 @@ describe('useGeminiStream', () => {
}
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledTimes(1);
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
});
@@ -590,7 +589,7 @@ describe('useGeminiStream', () => {
}
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
expect(client.addHistory).toHaveBeenCalledWith({
role: 'user',
@@ -702,7 +701,7 @@ describe('useGeminiStream', () => {
}
});
await waitFor(() => {
await vi.waitFor(() => {
// The tools should be marked as submitted locally
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith([
'cancel-1',
@@ -840,7 +839,7 @@ describe('useGeminiStream', () => {
});
// 5. Wait for submitQuery to be called
await waitFor(() => {
await vi.waitFor(() => {
expect(mockSendMessageStream).toHaveBeenCalledWith(
toolCallResponseParts,
expect.any(AbortSignal),
@@ -889,7 +888,7 @@ describe('useGeminiStream', () => {
});
// Wait for the first part of the response
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.streamingState).toBe(StreamingState.Responding);
});
@@ -897,7 +896,7 @@ describe('useGeminiStream', () => {
simulateEscapeKeyPress();
// Verify cancellation message is added
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
@@ -1030,7 +1029,7 @@ describe('useGeminiStream', () => {
result.current.submitQuery('long running query');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.streamingState).toBe(StreamingState.Responding);
});
@@ -1138,7 +1137,7 @@ describe('useGeminiStream', () => {
expect(mockCancelAllToolCalls).toHaveBeenCalled();
// A cancellation message should be added to history
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
text: 'Request cancelled.',
@@ -1167,7 +1166,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/memory add "test fact"');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockScheduleToolCalls).toHaveBeenCalledWith(
[
expect.objectContaining({
@@ -1194,7 +1193,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/help');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/help');
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
expect(mockSendMessageStream).not.toHaveBeenCalled(); // No LLM call made
@@ -1215,7 +1214,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/my-custom-command');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
'/my-custom-command',
);
@@ -1250,7 +1249,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/emptycmd');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/emptycmd');
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'',
@@ -1268,7 +1267,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('// This is a line comment');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'// This is a line comment',
@@ -1286,7 +1285,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/* This is a block comment */');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'/* This is a block comment */',
@@ -1324,7 +1323,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/about');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
});
});
@@ -1401,7 +1400,7 @@ describe('useGeminiStream', () => {
}
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockPerformMemoryRefresh).toHaveBeenCalledTimes(1);
});
});
@@ -1457,7 +1456,7 @@ describe('useGeminiStream', () => {
});
// 3. Assertion
await waitFor(() => {
await vi.waitFor(() => {
expect(mockParseAndFormatApiError).toHaveBeenCalledWith(
'Rate limit exceeded',
mockAuthType,
@@ -1990,7 +1989,7 @@ describe('useGeminiStream', () => {
});
// Check that the info message was added
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
@@ -2050,7 +2049,7 @@ describe('useGeminiStream', () => {
});
// Check that the message was added without suggestion
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
@@ -2105,7 +2104,7 @@ describe('useGeminiStream', () => {
});
// Check that the message was added with suggestion
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
@@ -2161,7 +2160,7 @@ describe('useGeminiStream', () => {
});
// Check that onCancelSubmit was called
await waitFor(() => {
await vi.waitFor(() => {
expect(onCancelSubmitSpy).toHaveBeenCalled();
});
});
@@ -2360,7 +2359,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery(`Test ${reason}`);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
@@ -2487,7 +2486,7 @@ describe('useGeminiStream', () => {
});
// Wait for the first response to complete
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'gemini',
@@ -2520,7 +2519,7 @@ describe('useGeminiStream', () => {
// We can verify this by checking that the LoadingIndicator would not show the previous thought
// The actual thought state is internal to the hook, but we can verify the behavior
// by ensuring the second response doesn't show the previous thought
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'gemini',
@@ -2638,7 +2637,7 @@ describe('useGeminiStream', () => {
});
// Verify cancellation message was added
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
@@ -2696,7 +2695,7 @@ describe('useGeminiStream', () => {
});
// Verify error message was added
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
@@ -2747,7 +2746,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('test query');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
expect(
typeof result.current.loopDetectionConfirmationRequest?.onComplete,
@@ -2795,7 +2794,7 @@ describe('useGeminiStream', () => {
});
// Wait for confirmation request to be set
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
});
@@ -2824,7 +2823,7 @@ describe('useGeminiStream', () => {
);
// Verify that the request was retried
await waitFor(() => {
await vi.waitFor(() => {
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
2,
@@ -2860,7 +2859,7 @@ describe('useGeminiStream', () => {
});
// Wait for confirmation request to be set
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
});
@@ -2907,7 +2906,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('first query');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
});
@@ -2957,7 +2956,7 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('second query');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
});
@@ -2980,7 +2979,7 @@ describe('useGeminiStream', () => {
);
// Verify that the request was retried
await waitFor(() => {
await vi.waitFor(() => {
expect(mockSendMessageStream).toHaveBeenCalledTimes(3); // 1st query, 2nd query, retry of 2nd query
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
3,
@@ -3011,7 +3010,7 @@ describe('useGeminiStream', () => {
});
// Verify that the content was added to history before the loop detection dialog
await waitFor(() => {
await vi.waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'gemini',
@@ -3022,7 +3021,7 @@ describe('useGeminiStream', () => {
});
// Then verify loop detection confirmation request was set
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
});
});
@@ -4,10 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useHistory } from './useHistoryManager.js';
import type { HistoryItem } from '../types.js';
@@ -4,9 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { act, renderHook } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useInputHistory } from './useInputHistory.js';
describe('useInputHistory', () => {
@@ -4,9 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { act, renderHook } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { useInputHistoryStore } from './useInputHistoryStore.js';
@@ -4,10 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
/// <reference types="vitest/globals" />
import {
describe,
it,
@@ -17,7 +13,8 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { usePermissionsModifyTrust } from './usePermissionsModifyTrust.js';
import { TrustLevel } from '../../config/trustedFolders.js';
import type { LoadedSettings } from '../../config/settings.js';
@@ -1,210 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* 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 {
usePhraseCycler,
WITTY_LOADING_PHRASES,
PHRASE_CHANGE_INTERVAL_MS,
} from './usePhraseCycler.js';
describe('usePhraseCycler', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should initialize with a witty phrase when not active and not waiting', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { result } = renderHook(() => usePhraseCycler(false, false));
expect(WITTY_LOADING_PHRASES).toContain(result.current);
});
it('should show "Waiting for user confirmation..." when isWaiting is true', () => {
const { result, rerender } = renderHook(
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
{ initialProps: { isActive: true, isWaiting: false } },
);
rerender({ isActive: true, isWaiting: true });
expect(result.current).toBe('Waiting for user confirmation...');
});
it('should not cycle phrases if isActive is false and not waiting', () => {
const { result } = renderHook(() => usePhraseCycler(false, false));
const initialPhrase = result.current;
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS * 2);
});
expect(result.current).toBe(initialPhrase);
});
it('should cycle through witty phrases when isActive is true and not waiting', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { result } = renderHook(() => usePhraseCycler(true, false));
// Initial phrase should be one of the witty phrases
expect(WITTY_LOADING_PHRASES).toContain(result.current);
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
});
// Phrase should change and be one of the witty phrases
expect(WITTY_LOADING_PHRASES).toContain(result.current);
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
});
expect(WITTY_LOADING_PHRASES).toContain(result.current);
});
it('should reset to a witty phrase when isActive becomes true after being false (and not waiting)', () => {
// Ensure there are at least two phrases for this test to be meaningful.
if (WITTY_LOADING_PHRASES.length < 2) {
return;
}
// Mock Math.random to make the test deterministic.
const mockRandomValues = [
0.5, // -> witty
0, // -> index 0
0.5, // -> witty
1 / WITTY_LOADING_PHRASES.length, // -> index 1
0.5, // -> witty
0, // -> index 0
];
let randomCallCount = 0;
vi.spyOn(Math, 'random').mockImplementation(() => {
const val = mockRandomValues[randomCallCount % mockRandomValues.length];
randomCallCount++;
return val;
});
const { result, rerender } = renderHook(
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
{ initialProps: { isActive: false, isWaiting: false } },
);
// Activate
rerender({ isActive: true, isWaiting: false });
const firstActivePhrase = result.current;
expect(WITTY_LOADING_PHRASES).toContain(firstActivePhrase);
// With our mock, this should be the first phrase.
expect(firstActivePhrase).toBe(WITTY_LOADING_PHRASES[0]);
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
});
// Phrase should change to the second phrase.
expect(result.current).not.toBe(firstActivePhrase);
expect(result.current).toBe(WITTY_LOADING_PHRASES[1]);
// Set to inactive - should reset to the default initial phrase
rerender({ isActive: false, isWaiting: false });
expect(WITTY_LOADING_PHRASES).toContain(result.current);
// Set back to active - should pick a random witty phrase (which our mock controls)
act(() => {
rerender({ isActive: true, isWaiting: false });
});
// The random mock will now return 0, so it should be the first phrase again.
expect(result.current).toBe(WITTY_LOADING_PHRASES[0]);
});
it('should clear phrase interval on unmount when active', () => {
const { unmount } = renderHook(() => usePhraseCycler(true, false));
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
unmount();
expect(clearIntervalSpy).toHaveBeenCalledOnce();
});
it('should use custom phrases when provided', () => {
const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2'];
let callCount = 0;
const randomMock = vi.spyOn(Math, 'random').mockImplementation(() => {
const val = callCount % 2;
callCount++;
return val / customPhrases.length;
});
const { result, rerender } = renderHook(
({ isActive, isWaiting, customPhrases: phrases }) =>
usePhraseCycler(isActive, isWaiting, phrases),
{
initialProps: {
isActive: true,
isWaiting: false,
customPhrases,
},
},
);
expect(result.current).toBe(customPhrases[0]);
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
});
expect(result.current).toBe(customPhrases[1]);
// Test fallback to default phrases.
randomMock.mockRestore();
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
rerender({ isActive: true, isWaiting: false, customPhrases: [] });
expect(WITTY_LOADING_PHRASES).toContain(result.current);
});
it('should fall back to witty phrases if custom phrases are an empty array', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { result } = renderHook(
({ isActive, isWaiting, customPhrases: phrases }) =>
usePhraseCycler(isActive, isWaiting, phrases),
{
initialProps: {
isActive: true,
isWaiting: false,
customPhrases: [],
},
},
);
expect(WITTY_LOADING_PHRASES).toContain(result.current);
});
it('should reset to a witty phrase when transitioning from waiting to active', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { result, rerender } = renderHook(
({ isActive, isWaiting }) => usePhraseCycler(isActive, isWaiting),
{ initialProps: { isActive: true, isWaiting: false } },
);
expect(WITTY_LOADING_PHRASES).toContain(result.current);
// Cycle to a different phrase (potentially)
act(() => {
vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS);
});
if (WITTY_LOADING_PHRASES.length > 1) {
// This check is probabilistic with random selection
}
expect(WITTY_LOADING_PHRASES).toContain(result.current);
// Go to waiting state
rerender({ isActive: false, isWaiting: true });
expect(result.current).toBe('Waiting for user confirmation...');
// Go back to active cycling - should pick a random witty phrase
rerender({ isActive: true, isWaiting: false });
expect(WITTY_LOADING_PHRASES).toContain(result.current);
});
});
@@ -0,0 +1,216 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act } from 'react';
import { render } from 'ink-testing-library';
import { Text } from 'ink';
import {
usePhraseCycler,
WITTY_LOADING_PHRASES,
PHRASE_CHANGE_INTERVAL_MS,
} from './usePhraseCycler.js';
// Test component to consume the hook
const TestComponent = ({
isActive,
isWaiting,
customPhrases,
}: {
isActive: boolean;
isWaiting: boolean;
customPhrases?: string[];
}) => {
const phrase = usePhraseCycler(isActive, isWaiting, customPhrases);
return <Text>{phrase}</Text>;
};
describe('usePhraseCycler', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should initialize with a witty phrase when not active and not waiting', () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { lastFrame } = render(
<TestComponent isActive={false} isWaiting={false} />,
);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
});
it('should show "Waiting for user confirmation..." when isWaiting is true', async () => {
const { lastFrame, rerender } = render(
<TestComponent isActive={true} isWaiting={false} />,
);
rerender(<TestComponent isActive={true} isWaiting={true} />);
await vi.advanceTimersByTimeAsync(0);
expect(lastFrame()).toBe('Waiting for user confirmation...');
});
it('should not cycle phrases if isActive is false and not waiting', async () => {
const { lastFrame } = render(
<TestComponent isActive={false} isWaiting={false} />,
);
const initialPhrase = lastFrame();
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS * 2);
expect(lastFrame()).toBe(initialPhrase);
});
it('should cycle through witty phrases when isActive is true and not waiting', async () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { lastFrame } = render(
<TestComponent isActive={true} isWaiting={false} />,
);
// Initial phrase should be one of the witty phrases
await vi.advanceTimersByTimeAsync(0);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
await act(async () => {
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
});
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
});
it('should reset to a phrase when isActive becomes true after being false', async () => {
const customPhrases = ['Phrase A', 'Phrase B'];
let callCount = 0;
vi.spyOn(Math, 'random').mockImplementation(() => {
// For custom phrases, only 1 Math.random call is made per update.
// 0 -> index 0 ('Phrase A')
// 0.99 -> index 1 ('Phrase B')
const val = callCount % 2 === 0 ? 0 : 0.99;
callCount++;
return val;
});
const { lastFrame, rerender } = render(
<TestComponent
isActive={false}
isWaiting={false}
customPhrases={customPhrases}
/>,
);
// Activate -> callCount 0 -> returns 0 -> 'Phrase A'
rerender(
<TestComponent
isActive={true}
isWaiting={false}
customPhrases={customPhrases}
/>,
);
await vi.advanceTimersByTimeAsync(0);
expect(lastFrame()).toBe('Phrase A');
// Interval -> callCount 1 -> returns 0.99 -> 'Phrase B'
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
expect(lastFrame()).toBe('Phrase B');
// Deactivate -> resets to customPhrases[0] -> 'Phrase A'
rerender(
<TestComponent
isActive={false}
isWaiting={false}
customPhrases={customPhrases}
/>,
);
await vi.advanceTimersByTimeAsync(0);
expect(lastFrame()).toBe('Phrase A');
// Activate again -> callCount 2 -> returns 0 -> 'Phrase A'
rerender(
<TestComponent
isActive={true}
isWaiting={false}
customPhrases={customPhrases}
/>,
);
await vi.advanceTimersByTimeAsync(0);
expect(lastFrame()).toBe('Phrase A');
});
it('should clear phrase interval on unmount when active', () => {
const { unmount } = render(
<TestComponent isActive={true} isWaiting={false} />,
);
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
unmount();
expect(clearIntervalSpy).toHaveBeenCalledOnce();
});
it('should use custom phrases when provided', async () => {
const customPhrases = ['Custom Phrase 1', 'Custom Phrase 2'];
const randomMock = vi.spyOn(Math, 'random');
randomMock.mockReturnValue(0);
const { lastFrame, rerender } = render(
<TestComponent
isActive={true}
isWaiting={false}
customPhrases={customPhrases}
/>,
);
expect(lastFrame()).toBe('Custom Phrase 1');
randomMock.mockReturnValue(0.99);
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
expect(lastFrame()).toBe('Custom Phrase 2');
// Test fallback to default phrases.
randomMock.mockRestore();
vi.spyOn(Math, 'random').mockReturnValue(0.5); // Always witty
rerender(
<TestComponent isActive={true} isWaiting={false} customPhrases={[]} />,
);
await vi.advanceTimersByTimeAsync(0);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
});
it('should fall back to witty phrases if custom phrases are an empty array', async () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { lastFrame } = render(
<TestComponent isActive={true} isWaiting={false} customPhrases={[]} />,
);
await vi.advanceTimersByTimeAsync(0);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
});
it('should reset to a witty phrase when transitioning from waiting to active', async () => {
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
const { lastFrame, rerender } = render(
<TestComponent isActive={true} isWaiting={false} />,
);
await vi.advanceTimersByTimeAsync(0);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
// Cycle to a different phrase (potentially)
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
// Go to waiting state
rerender(<TestComponent isActive={false} isWaiting={true} />);
await vi.advanceTimersByTimeAsync(0);
expect(lastFrame()).toBe('Waiting for user confirmation...');
// Go back to active cycling - should pick a random witty phrase
rerender(<TestComponent isActive={true} isWaiting={false} />);
await vi.advanceTimersByTimeAsync(0);
expect(WITTY_LOADING_PHRASES).toContain(lastFrame());
});
});
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import {
vi,
describe,
@@ -15,7 +13,8 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { act, renderHook } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import {
type Config,
type FallbackModelHandler,
@@ -4,11 +4,9 @@
* 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';
import { renderHook } from '../../test-utils/render.js';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { useReactToolScheduler } from './useReactToolScheduler.js';
@@ -4,10 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useReverseSearchCompletion } from './useReverseSearchCompletion.js';
import { useTextBuffer } from '../components/shared/text-buffer.js';
@@ -4,13 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useShellHistory } from './useShellHistory.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import * as crypto from 'node:crypto';
import { GEMINI_DIR } from '@google/gemini-cli-core';
@@ -19,7 +18,14 @@ vi.mock('node:fs/promises', () => ({
writeFile: vi.fn(),
mkdir: vi.fn(),
}));
vi.mock('node:os');
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:os')>();
return {
...actual,
homedir: mockHomedir,
};
});
vi.mock('node:crypto');
vi.mock('node:fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof import('node:fs')>();
@@ -33,6 +39,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
const path = await import('node:path');
class Storage {
static getGlobalSettingsPath(): string {
return '/test/home/.gemini/settings.json';
}
getProjectTempDir(): string {
return path.join('/test/home/', actual.GEMINI_DIR, 'tmp', 'mocked_hash');
}
@@ -68,7 +77,6 @@ const MOCKED_HISTORY_FILE = path.join(MOCKED_HISTORY_DIR, 'shell_history');
describe('useShellHistory', () => {
const mockedFs = vi.mocked(fs);
const mockedOs = vi.mocked(os);
const mockedCrypto = vi.mocked(crypto);
beforeEach(() => {
@@ -77,7 +85,7 @@ describe('useShellHistory', () => {
mockedFs.readFile.mockResolvedValue('');
mockedFs.writeFile.mockResolvedValue(undefined);
mockedFs.mkdir.mockResolvedValue(undefined);
mockedOs.homedir.mockReturnValue(MOCKED_HOME_DIR);
mockHomedir.mockReturnValue(MOCKED_HOME_DIR);
const hashMock = {
update: vi.fn().mockReturnThis(),
@@ -90,7 +98,7 @@ describe('useShellHistory', () => {
mockedFs.readFile.mockResolvedValue('cmd1\ncmd2');
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
await waitFor(() => {
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalledWith(
MOCKED_HISTORY_FILE,
'utf-8',
@@ -113,7 +121,7 @@ describe('useShellHistory', () => {
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
await waitFor(() => {
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
@@ -128,13 +136,15 @@ describe('useShellHistory', () => {
it('should add a command and write to the history file', async () => {
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
await waitFor(() => expect(mockedFs.readFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
act(() => {
result.current.addCommandToHistory('new_command');
});
await waitFor(() => {
await vi.waitFor(() => {
expect(mockedFs.mkdir).toHaveBeenCalledWith(MOCKED_HISTORY_DIR, {
recursive: true,
});
@@ -156,7 +166,9 @@ describe('useShellHistory', () => {
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
// Wait for history to be loaded: ['cmd3', 'cmd2', 'cmd1']
await waitFor(() => expect(mockedFs.readFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
let command: string | null = null;
@@ -200,7 +212,10 @@ describe('useShellHistory', () => {
it('should not add empty or whitespace-only commands to history', async () => {
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
await waitFor(() => expect(mockedFs.readFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
act(() => {
result.current.addCommandToHistory(' ');
@@ -214,14 +229,18 @@ describe('useShellHistory', () => {
mockedFs.readFile.mockResolvedValue(oldCommands.join('\n'));
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
await waitFor(() => expect(mockedFs.readFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
act(() => {
result.current.addCommandToHistory('new_cmd');
});
// Wait for the async write to happen and then inspect the arguments.
await waitFor(() => expect(mockedFs.writeFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.writeFile).toHaveBeenCalled();
});
// The hook stores history newest-first.
// Initial state: ['old_cmd_119', ..., 'old_cmd_0']
@@ -240,15 +259,20 @@ describe('useShellHistory', () => {
const { result } = renderHook(() => useShellHistory(MOCKED_PROJECT_ROOT));
// Initial state: ['cmd3', 'cmd2', 'cmd1']
await waitFor(() => expect(mockedFs.readFile).toHaveBeenCalled());
await vi.waitFor(() => {
expect(mockedFs.readFile).toHaveBeenCalled();
});
act(() => {
result.current.addCommandToHistory('cmd1');
});
// After re-adding 'cmd1': ['cmd1', 'cmd3', 'cmd2']
// Written to file (reversed): ['cmd2', 'cmd3', 'cmd1']
await waitFor(() => expect(mockedFs.writeFile).toHaveBeenCalled());
expect(mockedFs.readFile).toHaveBeenCalled();
await vi.waitFor(() => {
expect(mockedFs.writeFile).toHaveBeenCalled();
});
const writtenContent = mockedFs.writeFile.mock.calls[0][1] as string;
const writtenLines = writtenContent.split('\n');
@@ -4,10 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { describe, it, expect, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { renderHook } from '../../test-utils/render.js';
import { useSlashCompletion } from './useSlashCompletion.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import { CommandKind } from '../commands/types.js';
@@ -205,10 +203,12 @@ describe('useSlashCompletion', () => {
),
);
expect(result.current.suggestions.length).toBe(slashCommands.length);
expect(result.current.suggestions.map((s) => s.label)).toEqual(
expect.arrayContaining(['help', 'clear', 'memory', 'chat', 'stats']),
);
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBe(slashCommands.length);
expect(result.current.suggestions.map((s) => s.label)).toEqual(
expect.arrayContaining(['help', 'clear', 'memory', 'chat', 'stats']),
);
});
});
it('should filter commands based on partial input', async () => {
@@ -224,7 +224,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{
label: 'memory',
@@ -253,7 +253,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{
label: 'stats',
@@ -369,8 +369,10 @@ describe('useSlashCompletion', () => {
),
);
expect(result.current.suggestions.length).toBe(1);
expect(result.current.suggestions[0].label).toBe('visible');
await vi.waitFor(() => {
expect(result.current.suggestions.length).toBe(1);
expect(result.current.suggestions[0].label).toBe('visible');
});
});
});
@@ -390,29 +392,31 @@ describe('useSlashCompletion', () => {
const { result } = renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/memory',
'/memory ',
slashCommands,
mockCommandContext,
),
);
expect(result.current.suggestions).toHaveLength(2);
expect(result.current.suggestions).toEqual(
expect.arrayContaining([
{
label: 'show',
value: 'show',
description: 'Show memory',
commandKind: CommandKind.BUILT_IN,
},
{
label: 'add',
value: 'add',
description: 'Add to memory',
commandKind: CommandKind.BUILT_IN,
},
]),
);
await vi.waitFor(() => {
expect(result.current.suggestions).toHaveLength(2);
expect(result.current.suggestions).toEqual(
expect.arrayContaining([
{
label: 'show',
value: 'show',
description: 'Show memory',
commandKind: CommandKind.BUILT_IN,
},
{
label: 'add',
value: 'add',
description: 'Add to memory',
commandKind: CommandKind.BUILT_IN,
},
]),
);
});
});
it('should suggest all sub-commands when the query ends with the parent command and a space', async () => {
@@ -435,23 +439,25 @@ describe('useSlashCompletion', () => {
),
);
expect(result.current.suggestions).toHaveLength(2);
expect(result.current.suggestions).toEqual(
expect.arrayContaining([
{
label: 'show',
value: 'show',
description: 'Show memory',
commandKind: CommandKind.BUILT_IN,
},
{
label: 'add',
value: 'add',
description: 'Add to memory',
commandKind: CommandKind.BUILT_IN,
},
]),
);
await vi.waitFor(() => {
expect(result.current.suggestions).toHaveLength(2);
expect(result.current.suggestions).toEqual(
expect.arrayContaining([
{
label: 'show',
value: 'show',
description: 'Show memory',
commandKind: CommandKind.BUILT_IN,
},
{
label: 'add',
value: 'add',
description: 'Add to memory',
commandKind: CommandKind.BUILT_IN,
},
]),
);
});
});
it('should filter sub-commands by prefix', async () => {
@@ -474,7 +480,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{
label: 'add',
@@ -547,7 +553,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(mockCompletionFn).toHaveBeenCalledWith(
expect.objectContaining({
invocation: {
@@ -560,7 +566,7 @@ describe('useSlashCompletion', () => {
);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{ label: 'my-chat-tag-1', value: 'my-chat-tag-1' },
{ label: 'my-chat-tag-2', value: 'my-chat-tag-2' },
@@ -596,7 +602,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(mockCompletionFn).toHaveBeenCalledWith(
expect.objectContaining({
invocation: {
@@ -609,7 +615,7 @@ describe('useSlashCompletion', () => {
);
});
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toHaveLength(3);
});
});
@@ -639,9 +645,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
expect(result.current.suggestions).toHaveLength(0);
});
expect(result.current.suggestions).toHaveLength(0);
});
});
@@ -714,7 +718,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{
label: 'summarize',
@@ -795,7 +799,7 @@ describe('useSlashCompletion', () => {
),
);
await waitFor(() => {
await vi.waitFor(() => {
expect(result.current.suggestions).toEqual([
{
label: 'custom-script',
@@ -4,12 +4,11 @@
* 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';
import { renderHook, act } from '@testing-library/react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import {
useReactToolScheduler,
mapToDisplay,
@@ -38,7 +37,14 @@ import { ToolCallStatus } from '../types.js';
// Mocks
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
const actual = await vi.importActual<any>('@google/gemini-cli-core');
// Patch CoreToolScheduler to have cancelAll if it's missing in the test environment
if (
actual.CoreToolScheduler &&
!actual.CoreToolScheduler.prototype.cancelAll
) {
actual.CoreToolScheduler.prototype.cancelAll = vi.fn();
}
return {
...actual,
ToolRegistry: vi.fn(),
@@ -153,13 +159,13 @@ describe('useReactToolScheduler in YOLO Mode', () => {
});
await act(async () => {
await vi.runAllTimersAsync(); // Process validation
await vi.advanceTimersByTimeAsync(0); // Process validation
});
await act(async () => {
await vi.runAllTimersAsync(); // Process scheduling
await vi.advanceTimersByTimeAsync(0); // Process scheduling
});
await act(async () => {
await vi.runAllTimersAsync(); // Process execution
await vi.advanceTimersByTimeAsync(0); // Process execution
});
// Check that execute WAS called
@@ -270,13 +276,13 @@ describe('useReactToolScheduler', () => {
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(mockTool.execute).toHaveBeenCalledWith(request.args);
@@ -341,13 +347,13 @@ describe('useReactToolScheduler', () => {
// Let the new call finish.
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(onComplete).toHaveBeenCalled();
});
@@ -375,11 +381,11 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
}); // validation
await act(async () => {
await vi.runAllTimersAsync();
}); // scheduling
await vi.advanceTimersByTimeAsync(0); // Process scheduling
});
// At this point, the tool is 'executing' and waiting on the promise.
expect(result.current[0][0].status).toBe('executing');
@@ -390,7 +396,7 @@ describe('useReactToolScheduler', () => {
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(onComplete).toHaveBeenCalledWith([
@@ -423,10 +429,10 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(completedToolCalls).toHaveLength(1);
@@ -462,10 +468,10 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(completedToolCalls).toHaveLength(1);
@@ -497,13 +503,13 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(completedToolCalls).toHaveLength(1);
@@ -532,7 +538,7 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
const waitingCall = result.current[0][0] as any;
@@ -545,13 +551,13 @@ describe('useReactToolScheduler', () => {
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
@@ -590,7 +596,7 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
const waitingCall = result.current[0][0] as any;
@@ -602,10 +608,10 @@ describe('useReactToolScheduler', () => {
await capturedOnConfirmForTest?.(ToolConfirmationOutcome.Cancel);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
@@ -665,7 +671,7 @@ describe('useReactToolScheduler', () => {
schedule(request, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(liveUpdateFn).toBeDefined();
@@ -675,14 +681,14 @@ describe('useReactToolScheduler', () => {
liveUpdateFn?.('Live output 1');
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
liveUpdateFn?.('Live output 2');
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
act(() => {
@@ -692,10 +698,10 @@ describe('useReactToolScheduler', () => {
} as ToolResult);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(onComplete).toHaveBeenCalledWith([
@@ -753,16 +759,16 @@ describe('useReactToolScheduler', () => {
schedule(requests, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
expect(onComplete).toHaveBeenCalledTimes(1);
@@ -845,16 +851,16 @@ describe('useReactToolScheduler', () => {
schedule(request1, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
schedule(request2, new AbortController().signal);
await act(async () => {
await vi.advanceTimersByTimeAsync(50);
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
});
expect(onComplete).toHaveBeenCalledWith([
@@ -867,9 +873,9 @@ describe('useReactToolScheduler', () => {
// Wait for request2 to complete
await act(async () => {
await vi.advanceTimersByTimeAsync(50);
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
await act(async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
});
});
expect(onComplete).toHaveBeenCalledWith([