diff --git a/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.test.tsx b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.test.tsx new file mode 100644 index 0000000000..e077f64e8e --- /dev/null +++ b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.test.tsx @@ -0,0 +1,142 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../test-utils/render.js'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { CloudFreePrivacyNotice } from './CloudFreePrivacyNotice.js'; +import { usePrivacySettings } from '../hooks/usePrivacySettings.js'; +import { useKeypress } from '../hooks/useKeypress.js'; +import type { Config } from '@google/gemini-cli-core'; +import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; + +// Mocks +vi.mock('../hooks/usePrivacySettings.js', () => ({ + usePrivacySettings: vi.fn(), +})); + +vi.mock('../components/shared/RadioButtonSelect.js', () => ({ + RadioButtonSelect: vi.fn(), +})); + +vi.mock('../hooks/useKeypress.js', () => ({ + useKeypress: vi.fn(), +})); + +const mockedUsePrivacySettings = usePrivacySettings as Mock; +const mockedUseKeypress = useKeypress as Mock; +const mockedRadioButtonSelect = RadioButtonSelect as Mock; + +describe('CloudFreePrivacyNotice', () => { + const mockConfig = {} as Config; + const onExit = vi.fn(); + const updateDataCollectionOptIn = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + mockedUsePrivacySettings.mockReturnValue({ + privacyState: { + isLoading: false, + error: undefined, + isFreeTier: true, + dataCollectionOptIn: undefined, + }, + updateDataCollectionOptIn, + }); + }); + + const defaultState = { + isLoading: false, + error: undefined, + isFreeTier: true, + dataCollectionOptIn: undefined, + }; + + it.each([ + { + stateName: 'loading state', + mockState: { isLoading: true }, + expectedText: 'Loading...', + }, + { + stateName: 'error state', + mockState: { error: 'Something went wrong' }, + expectedText: 'Error loading Opt-in settings', + }, + { + stateName: 'non-free tier state', + mockState: { isFreeTier: false }, + expectedText: 'Gemini Code Assist Privacy Notice', + }, + { + stateName: 'free tier state', + mockState: { isFreeTier: true }, + expectedText: 'Gemini Code Assist for Individuals Privacy Notice', + }, + ])('renders correctly in $stateName', ({ mockState, expectedText }) => { + mockedUsePrivacySettings.mockReturnValue({ + privacyState: { ...defaultState, ...mockState }, + updateDataCollectionOptIn, + }); + + const { lastFrame } = render( + , + ); + + expect(lastFrame()).toContain(expectedText); + }); + + it.each([ + { + stateName: 'error state', + mockState: { error: 'Something went wrong' }, + shouldExit: true, + }, + { + stateName: 'non-free tier state', + mockState: { isFreeTier: false }, + shouldExit: true, + }, + { + stateName: 'free tier state (no selection)', + mockState: { isFreeTier: true }, + shouldExit: false, + }, + ])( + 'exits on Escape in $stateName: $shouldExit', + ({ mockState, shouldExit }) => { + mockedUsePrivacySettings.mockReturnValue({ + privacyState: { ...defaultState, ...mockState }, + updateDataCollectionOptIn, + }); + + render(); + + const keypressHandler = mockedUseKeypress.mock.calls[0][0]; + keypressHandler({ name: 'escape' }); + + if (shouldExit) { + expect(onExit).toHaveBeenCalled(); + } else { + expect(onExit).not.toHaveBeenCalled(); + } + }, + ); + + describe('RadioButtonSelect interaction', () => { + it.each([ + { selection: true, label: 'Yes' }, + { selection: false, label: 'No' }, + ])('calls correct functions on selecting "$label"', ({ selection }) => { + render(); + + const onSelectHandler = mockedRadioButtonSelect.mock.calls[0][0].onSelect; + onSelectHandler(selection); + + expect(updateDataCollectionOptIn).toHaveBeenCalledWith(selection); + expect(onExit).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.test.tsx b/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.test.tsx new file mode 100644 index 0000000000..0aab0e18c4 --- /dev/null +++ b/packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.test.tsx @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../test-utils/render.js'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { CloudPaidPrivacyNotice } from './CloudPaidPrivacyNotice.js'; +import { useKeypress } from '../hooks/useKeypress.js'; + +// Mocks +vi.mock('../hooks/useKeypress.js', () => ({ + useKeypress: vi.fn(), +})); + +const mockedUseKeypress = useKeypress as Mock; + +describe('CloudPaidPrivacyNotice', () => { + const onExit = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders correctly', () => { + const { lastFrame } = render(); + + expect(lastFrame()).toContain('Vertex AI Notice'); + expect(lastFrame()).toContain('Service Specific Terms'); + expect(lastFrame()).toContain('Press Esc to exit'); + }); + + it('exits on Escape', () => { + render(); + + const keypressHandler = mockedUseKeypress.mock.calls[0][0]; + keypressHandler({ name: 'escape' }); + + expect(onExit).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/ui/privacy/GeminiPrivacyNotice.test.tsx b/packages/cli/src/ui/privacy/GeminiPrivacyNotice.test.tsx new file mode 100644 index 0000000000..3548aaa615 --- /dev/null +++ b/packages/cli/src/ui/privacy/GeminiPrivacyNotice.test.tsx @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../test-utils/render.js'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { GeminiPrivacyNotice } from './GeminiPrivacyNotice.js'; +import { useKeypress } from '../hooks/useKeypress.js'; + +// Mocks +vi.mock('../hooks/useKeypress.js', () => ({ + useKeypress: vi.fn(), +})); + +const mockedUseKeypress = useKeypress as Mock; + +describe('GeminiPrivacyNotice', () => { + const onExit = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders correctly', () => { + const { lastFrame } = render(); + + expect(lastFrame()).toContain('Gemini API Key Notice'); + expect(lastFrame()).toContain('By using the Gemini API'); + expect(lastFrame()).toContain('Press Esc to exit'); + }); + + it('exits on Escape', () => { + render(); + + const keypressHandler = mockedUseKeypress.mock.calls[0][0]; + keypressHandler({ name: 'escape' }); + + expect(onExit).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/ui/privacy/PrivacyNotice.test.tsx b/packages/cli/src/ui/privacy/PrivacyNotice.test.tsx new file mode 100644 index 0000000000..c12e5fd622 --- /dev/null +++ b/packages/cli/src/ui/privacy/PrivacyNotice.test.tsx @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../test-utils/render.js'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { PrivacyNotice } from './PrivacyNotice.js'; +import type { + AuthType, + Config, + ContentGeneratorConfig, +} from '@google/gemini-cli-core'; + +// Mock child components +vi.mock('./GeminiPrivacyNotice.js', async () => { + const { Text } = await import('ink'); + return { + GeminiPrivacyNotice: () => GeminiPrivacyNotice, + }; +}); + +vi.mock('./CloudPaidPrivacyNotice.js', async () => { + const { Text } = await import('ink'); + return { + CloudPaidPrivacyNotice: () => CloudPaidPrivacyNotice, + }; +}); + +vi.mock('./CloudFreePrivacyNotice.js', async () => { + const { Text } = await import('ink'); + return { + CloudFreePrivacyNotice: () => CloudFreePrivacyNotice, + }; +}); + +describe('PrivacyNotice', () => { + const onExit = vi.fn(); + const mockConfig = { + getContentGeneratorConfig: vi.fn(), + } as unknown as Config; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it.each([ + { + authType: 'gemini-api-key' as AuthType, + expectedComponent: 'GeminiPrivacyNotice', + }, + { + authType: 'vertex-ai' as AuthType, + expectedComponent: 'CloudPaidPrivacyNotice', + }, + { + authType: 'oauth-personal' as AuthType, + expectedComponent: 'CloudFreePrivacyNotice', + }, + { + authType: 'UNKNOWN' as AuthType, + expectedComponent: 'CloudFreePrivacyNotice', + }, + ])( + 'renders $expectedComponent when authType is $authType', + ({ authType, expectedComponent }) => { + vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({ + authType, + } as unknown as ContentGeneratorConfig); + + const { lastFrame } = render( + , + ); + + expect(lastFrame()).toContain(expectedComponent); + }, + ); +});