Skip to content

Commit 54fa26e

Browse files
authored
Fix tests to wrap all calls changing the UI with act. (#12268)
1 parent cc08133 commit 54fa26e

69 files changed

Lines changed: 1955 additions & 1244 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/a2a-server/vitest.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export default defineConfig({
1111
test: {
1212
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
1313
exclude: ['**/node_modules/**', '**/dist/**'],
14-
environment: 'jsdom',
1514
globals: true,
1615
reporters: ['default', 'junit'],
1716
silent: true,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { act } from 'react';
8+
9+
// The waitFor from vitest doesn't properly wrap in act(), so we have to
10+
// implement our own like the one in @testing-library/react
11+
// or @testing-library/react-native
12+
// The version of waitFor from vitest is still fine to use if you aren't waiting
13+
// for React state updates.
14+
export async function waitFor(
15+
assertion: () => void,
16+
{ timeout = 1000, interval = 50 } = {},
17+
): Promise<void> {
18+
const startTime = Date.now();
19+
20+
while (true) {
21+
try {
22+
assertion();
23+
return;
24+
} catch (error) {
25+
if (Date.now() - startTime > timeout) {
26+
throw error;
27+
}
28+
29+
await act(async () => {
30+
await new Promise((resolve) => setTimeout(resolve, interval));
31+
});
32+
}
33+
}
34+
}

packages/cli/src/test-utils/render.test.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,37 @@
66

77
import { describe, it, expect, vi } from 'vitest';
88
import { useState, useEffect } from 'react';
9-
import { renderHook } from './render.js';
9+
import { Text } from 'ink';
10+
import { renderHook, render } from './render.js';
11+
import { waitFor } from './async.js';
12+
13+
describe('render', () => {
14+
it('should render a component', () => {
15+
const { lastFrame } = render(<Text>Hello World</Text>);
16+
expect(lastFrame()).toBe('Hello World');
17+
});
18+
19+
it('should support rerender', () => {
20+
const { lastFrame, rerender } = render(<Text>Hello</Text>);
21+
expect(lastFrame()).toBe('Hello');
22+
23+
rerender(<Text>World</Text>);
24+
expect(lastFrame()).toBe('World');
25+
});
26+
27+
it('should support unmount', () => {
28+
const cleanup = vi.fn();
29+
function TestComponent() {
30+
useEffect(() => cleanup, []);
31+
return <Text>Hello</Text>;
32+
}
33+
34+
const { unmount } = render(<TestComponent />);
35+
unmount();
36+
37+
expect(cleanup).toHaveBeenCalled();
38+
});
39+
});
1040

1141
describe('renderHook', () => {
1242
it('should rerender with previous props when called without arguments', async () => {
@@ -23,19 +53,19 @@ describe('renderHook', () => {
2353
});
2454

2555
expect(result.current.value).toBe(1);
26-
await vi.waitFor(() => expect(result.current.count).toBe(1));
56+
await waitFor(() => expect(result.current.count).toBe(1));
2757

2858
// Rerender with new props
2959
rerender({ value: 2 });
3060
expect(result.current.value).toBe(2);
31-
await vi.waitFor(() => expect(result.current.count).toBe(2));
61+
await waitFor(() => expect(result.current.count).toBe(2));
3262

3363
// Rerender without arguments should use previous props (value: 2)
3464
// This would previously crash or pass undefined if not fixed
3565
rerender();
3666
expect(result.current.value).toBe(2);
3767
// Count should not increase because value didn't change
38-
await vi.waitFor(() => expect(result.current.count).toBe(2));
68+
await waitFor(() => expect(result.current.count).toBe(2));
3969
});
4070

4171
it('should handle initial render without props', () => {

packages/cli/src/test-utils/render.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { render } from 'ink-testing-library';
7+
import { render as inkRender } from 'ink-testing-library';
88
import type React from 'react';
99
import { act } from 'react';
1010
import { LoadedSettings, type Settings } from '../config/settings.js';
@@ -19,6 +19,34 @@ import { VimModeProvider } from '../ui/contexts/VimModeContext.js';
1919

2020
import { type Config } from '@google/gemini-cli-core';
2121

22+
// Wrapper around ink-testing-library's render that ensures act() is called
23+
export const render = (
24+
tree: React.ReactElement,
25+
): ReturnType<typeof inkRender> => {
26+
let renderResult: ReturnType<typeof inkRender> =
27+
undefined as unknown as ReturnType<typeof inkRender>;
28+
act(() => {
29+
renderResult = inkRender(tree);
30+
});
31+
32+
const originalUnmount = renderResult.unmount;
33+
const originalRerender = renderResult.rerender;
34+
35+
return {
36+
...renderResult,
37+
unmount: () => {
38+
act(() => {
39+
originalUnmount();
40+
});
41+
},
42+
rerender: (newTree: React.ReactElement) => {
43+
act(() => {
44+
originalRerender(newTree);
45+
});
46+
},
47+
};
48+
};
49+
2250
const mockConfig = {
2351
getModel: () => 'gemini-pro',
2452
getTargetDir: () =>

packages/cli/src/ui/App.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { describe, it, expect, vi, type Mock } from 'vitest';
8-
import { render } from 'ink-testing-library';
8+
import { render } from '../test-utils/render.js';
99
import { Text, useIsScreenReaderEnabled } from 'ink';
1010
import { makeFakeConfig } from '@google/gemini-cli-core';
1111
import { App } from './App.js';

0 commit comments

Comments
 (0)