mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-30 07:51:07 -07:00
test(cli): refactor tests for async render utilities (#23252)
This commit is contained in:
committed by
GitHub
parent
86a3a913b5
commit
6c78eb7a39
@@ -73,23 +73,21 @@ describe('ApiAuthDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with a defaultValue', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
defaultValue="test-key"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(mockedUseTextBuffer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialText: 'test-key',
|
||||
@@ -113,10 +111,9 @@ describe('ApiAuthDialog', () => {
|
||||
'calls $expectedCall.name when $keyName is pressed',
|
||||
async ({ keyName, sequence, expectedCall, args }) => {
|
||||
mockBuffer.text = 'submitted-key'; // Set for the onSubmit case
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// calls[0] is the ApiAuthDialog's useKeypress (Ctrl+C handler)
|
||||
// calls[1] is the TextInput's useKeypress (typing handler)
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[1][0];
|
||||
@@ -136,24 +133,22 @@ describe('ApiAuthDialog', () => {
|
||||
);
|
||||
|
||||
it('displays an error message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ApiAuthDialog
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
error="Invalid API Key"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Invalid API Key');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls clearApiKey and clears buffer when Ctrl+C is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// Call 0 is ApiAuthDialog (isActive: true)
|
||||
// Call 1 is TextInput (isActive: true, priority: true)
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
@@ -143,10 +143,9 @@ describe('AuthDialog', () => {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
vi.stubEnv(key, value as string);
|
||||
}
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
for (const item of shouldContain) {
|
||||
expect(items).toContainEqual(item);
|
||||
@@ -161,10 +160,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('filters auth types when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].value).toBe(AuthType.USE_GEMINI);
|
||||
@@ -173,10 +169,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets initial index to 0 when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(initialIndex).toBe(0);
|
||||
unmount();
|
||||
@@ -213,10 +206,7 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
])('selects initial auth type $desc', async ({ setup, expected }) => {
|
||||
setup();
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(items[initialIndex].value).toBe(expected);
|
||||
unmount();
|
||||
@@ -226,10 +216,7 @@ describe('AuthDialog', () => {
|
||||
describe('handleAuthSelect', () => {
|
||||
it('calls onAuthError if validation fails', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue('Invalid method');
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -245,10 +232,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||
@@ -261,10 +245,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with empty object for other auth types', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -278,10 +259,7 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -297,10 +275,7 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
|
||||
// props.settings.merged.security.auth.selectedType is undefined here
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -316,10 +291,7 @@ describe('AuthDialog', () => {
|
||||
// process.env['GEMINI_API_KEY'] is not set
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -337,10 +309,7 @@ describe('AuthDialog', () => {
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -360,10 +329,7 @@ describe('AuthDialog', () => {
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -383,10 +349,9 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('displays authError when provided', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Something went wrong');
|
||||
unmount();
|
||||
});
|
||||
@@ -429,10 +394,7 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
])('$desc', async ({ setup, expectations }) => {
|
||||
setup();
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
keypressHandler({ name: 'escape' });
|
||||
expectations(props);
|
||||
@@ -442,30 +404,27 @@ describe('AuthDialog', () => {
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it('renders correctly with default props', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with auth error', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with enforced auth type', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -55,20 +55,18 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('renders initial state with spinner', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('[Spinner] Waiting for authentication...');
|
||||
expect(lastFrame()).toContain('Press Esc or Ctrl+C to cancel');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onTimeout when ESC is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
@@ -84,10 +82,9 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('calls onTimeout when Ctrl+C is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
@@ -100,10 +97,9 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('calls onTimeout and shows timeout message after 3 minutes', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(180000);
|
||||
@@ -116,10 +112,7 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('clears timer on unmount', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
|
||||
await act(async () => {
|
||||
unmount();
|
||||
|
||||
@@ -73,14 +73,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders the suspension message from accountSuspensionInfo', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Account Suspended');
|
||||
expect(frame).toContain('violation of Terms of Service');
|
||||
@@ -89,14 +88,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders menu options with appeal link text from response', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(3);
|
||||
expect(items[0].label).toBe('Appeal Here');
|
||||
@@ -109,14 +107,13 @@ describe('BannedAccountDialog', () => {
|
||||
const infoWithoutUrl: AccountSuspensionInfo = {
|
||||
message: 'Account suspended.',
|
||||
};
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={infoWithoutUrl}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0].label).toBe('Change authentication');
|
||||
@@ -129,28 +126,26 @@ describe('BannedAccountDialog', () => {
|
||||
message: 'Account suspended.',
|
||||
appealUrl: 'https://example.com/appeal',
|
||||
};
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={infoWithoutLinkText}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items[0].label).toBe('Open the Google Form');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('opens browser when appeal option is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await onSelect('open_form');
|
||||
expect(mockedOpenBrowser).toHaveBeenCalledWith(
|
||||
@@ -162,14 +157,13 @@ describe('BannedAccountDialog', () => {
|
||||
|
||||
it('shows URL when browser cannot be launched', async () => {
|
||||
mockedShouldLaunchBrowser.mockReturnValue(false);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
onSelect('open_form');
|
||||
await waitFor(() => {
|
||||
@@ -180,14 +174,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('calls onExit when "Exit" is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await onSelect('exit');
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||
@@ -196,14 +189,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('calls onChangeAuth when "Change authentication" is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
onSelect('change_auth');
|
||||
expect(onChangeAuth).toHaveBeenCalled();
|
||||
@@ -212,14 +204,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('exits on escape key', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
const result = keypressHandler({ name: 'escape' });
|
||||
expect(result).toBe(true);
|
||||
@@ -227,14 +218,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders snapshot correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -45,25 +45,23 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onDismiss when escape is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
@@ -83,13 +81,12 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useAuthCommand, validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import {
|
||||
@@ -22,7 +15,6 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { AuthState } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockLoadApiKey = vi.fn();
|
||||
@@ -142,171 +134,202 @@ describe('useAuth', () => {
|
||||
},
|
||||
}) as LoadedSettings;
|
||||
|
||||
let deferredRefreshAuth: {
|
||||
resolve: () => void;
|
||||
reject: (e: Error) => void;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(mockConfig.refreshAuth).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
deferredRefreshAuth = { resolve, reject };
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize with Unauthenticated state', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
// Because we defer refreshAuth, the initial state is safely caught here
|
||||
expect(result.current.authState).toBe(AuthState.Unauthenticated);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
});
|
||||
|
||||
it('should set error if no auth type is selected and no env key', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(undefined), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe(
|
||||
'No authentication method selected.',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
// This happens synchronously, no deferred promise
|
||||
expect(result.current.authError).toBe(
|
||||
'No authentication method selected.',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should set error if no auth type is selected but env key exists', async () => {
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(undefined), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain(
|
||||
'Existing API key detected (GEMINI_API_KEY)',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toContain(
|
||||
'Existing API key detected (GEMINI_API_KEY)',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should transition to AwaitingApiKeyInput if USE_GEMINI and no key found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue(null);
|
||||
const { result } = renderHook(() =>
|
||||
let deferredLoadKey: { resolve: (k: string | null) => void };
|
||||
mockLoadApiKey.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
deferredLoadKey = { resolve };
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput);
|
||||
await act(async () => {
|
||||
deferredLoadKey.resolve(null);
|
||||
});
|
||||
|
||||
expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput);
|
||||
});
|
||||
|
||||
it('should authenticate if USE_GEMINI and key is found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue('stored-key');
|
||||
const { result } = renderHook(() =>
|
||||
let deferredLoadKey: { resolve: (k: string | null) => void };
|
||||
mockLoadApiKey.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
deferredLoadKey = { resolve };
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('stored-key');
|
||||
await act(async () => {
|
||||
deferredLoadKey.resolve('stored-key');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('stored-key');
|
||||
});
|
||||
|
||||
it('should authenticate if USE_GEMINI and env key is found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue(null);
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
});
|
||||
|
||||
it('should prioritize env key over stored key when both are present', async () => {
|
||||
mockLoadApiKey.mockResolvedValue('stored-key');
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
// The environment key should take precedence
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
});
|
||||
|
||||
it('should set error if validation fails', async () => {
|
||||
mockValidateAuthMethod.mockReturnValue('Validation Failed');
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe('Validation Failed');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toBe('Validation Failed');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should set error if GEMINI_DEFAULT_AUTH_TYPE is invalid', async () => {
|
||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = 'INVALID_TYPE';
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain(
|
||||
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toContain(
|
||||
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should authenticate successfully for valid auth type', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.authError).toBeNull();
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.authError).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle refreshAuth failure', async () => {
|
||||
(mockConfig.refreshAuth as Mock).mockRejectedValue(
|
||||
new Error('Auth Failed'),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain('Failed to sign in');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.reject(new Error('Auth Failed'));
|
||||
});
|
||||
|
||||
expect(result.current.authError).toContain('Failed to sign in');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should handle ProjectIdRequiredError without "Failed to login" prefix', async () => {
|
||||
const projectIdError = new ProjectIdRequiredError();
|
||||
(mockConfig.refreshAuth as Mock).mockRejectedValue(projectIdError);
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe(
|
||||
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
|
||||
);
|
||||
expect(result.current.authError).not.toContain('Failed to login');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.reject(projectIdError);
|
||||
});
|
||||
|
||||
expect(result.current.authError).toBe(
|
||||
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
|
||||
);
|
||||
expect(result.current.authError).not.toContain('Failed to login');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user