import React from 'react' import { Linking } from 'react-native' import { act, create, type ReactTestRenderer } from 'react-test-renderer' const mockGetAuthCapabilities = jest.fn() const mockSignInWithOAuthProvider = jest.fn() const mockNavigation = { canGoBack: jest.fn(() => true), goBack: jest.fn(), navigate: jest.fn(), } jest.mock('@react-navigation/native', () => ({ useNavigation: () => mockNavigation, })) jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), })) jest.mock('@d3ro/i18n', () => ({ useI18n: () => ({ locale: 'ko', t: (key: string) => key, }), })) jest.mock('../src/lib/preferences-context', () => ({ useMobilePreferences: () => ({ palette: { bg: { app: '#000', card: '#181818', inset: '#202020', cardHover: '#222' }, text: { primary: '#fff', muted: '#999', onAccent: '#000' }, border: { default: '#444', strong: '#666' }, accent: { main: '#fc0' }, tag: { red: '#f66' }, }, }), })) jest.mock('../src/lib/auth-context', () => ({ useAuth: () => ({ authError: null }), })) jest.mock('../src/lib/auth-capabilities', () => ({ getAuthCapabilities: (...args: unknown[]) => mockGetAuthCapabilities(...args), })) jest.mock('../src/lib/supabase', () => ({ isSupabaseConfigured: () => true, supabase: { auth: { signUp: jest.fn(), resend: jest.fn(), }, }, })) jest.mock('../src/lib/auth-redirect', () => ({ AUTH_REDIRECT_URL: 'd3ro-voice://auth-callback', })) jest.mock('../src/features/auth/oauth-sign-in', () => { class OAuthSignInError extends Error { readonly code: 'oauth_url_failed' | 'oauth_callback_failed' constructor(code: 'oauth_url_failed' | 'oauth_callback_failed') { super(code) this.code = code } } return { OAuthSignInError, signInWithOAuthProvider: (...args: unknown[]) => mockSignInWithOAuthProvider(...args), } }) import SignUpScreen from '../src/screens/SignUpScreen' const ENABLED_CAPABILITIES = { signUpEnabled: true, emailEnabled: true, googleEnabled: true, githubEnabled: false, appleEnabled: false, } async function renderScreen(): Promise { let renderer: ReactTestRenderer await act(async () => { renderer = create() await Promise.resolve() await Promise.resolve() }) return renderer! } describe('SignUpScreen Google OAuth discoverability', () => { beforeEach(() => { mockGetAuthCapabilities.mockReset().mockResolvedValue(ENABLED_CAPABILITIES) mockSignInWithOAuthProvider.mockReset().mockResolvedValue('cancelled') jest.spyOn(Linking, 'openURL').mockResolvedValue(true) }) afterEach(() => { jest.restoreAllMocks() }) it('shows an accessible Google sign-up CTA only when the server enables it', async () => { const enabled = await renderScreen() const button = enabled.root.findByProps({ testID: 'signup-oauth-google' }) expect(button.props.accessibilityLabel).toBe('mobile.signup.google') expect(button.props.accessibilityHint).toBe('mobile.signup.googleHint') expect(button.props.accessibilityState).toEqual({ disabled: true, busy: false }) expect(enabled.root.findByProps({ testID: 'sign-up-terms' })).toBeTruthy() expect(enabled.root.findByProps({ testID: 'sign-up-submit' })).toBeTruthy() mockGetAuthCapabilities.mockReset().mockResolvedValue({ ...ENABLED_CAPABILITIES, signUpEnabled: false, }) const existingGoogleAccount = await renderScreen() expect(existingGoogleAccount.root.findByProps({ testID: 'signup-oauth-google' })).toBeTruthy() mockGetAuthCapabilities.mockReset().mockResolvedValue({ ...ENABLED_CAPABILITIES, googleEnabled: false, }) const disabled = await renderScreen() expect(() => disabled.root.findByProps({ testID: 'signup-oauth-google' })).toThrow() expect(disabled.root.findByProps({ testID: 'sign-up-submit' })).toBeTruthy() }) it('locks both sign-up paths while OAuth is active and treats cancellation as non-fatal', async () => { let resolveOAuth: ((value: 'cancelled') => void) | null = null mockSignInWithOAuthProvider.mockReturnValue(new Promise((resolve) => { resolveOAuth = resolve })) const renderer = await renderScreen() await act(async () => { renderer.root.findByProps({ testID: 'sign-up-terms' }).props.onPress() }) const button = renderer.root.findByProps({ testID: 'signup-oauth-google' }) await act(async () => { button.props.onPress() await Promise.resolve() }) expect(mockSignInWithOAuthProvider).toHaveBeenCalledWith('google') expect(renderer.root.findByProps({ testID: 'signup-oauth-google' }).props.disabled).toBe(true) expect(renderer.root.findByProps({ testID: 'signup-oauth-google' }).props.accessibilityState) .toEqual({ disabled: true, busy: true }) expect(renderer.root.findByProps({ testID: 'signup-oauth-google-loading' })).toBeTruthy() expect(renderer.root.findByProps({ testID: 'sign-up-submit' }).props.disabled).toBe(true) await act(async () => { resolveOAuth?.('cancelled') await Promise.resolve() }) expect(renderer.root.findByProps({ testID: 'signup-oauth-google' }).props.disabled).toBe(false) expect(() => renderer.root.findByProps({ testID: 'auth-error' })).toThrow() }) it('renders a localized fail-closed error without removing email sign-up', async () => { mockSignInWithOAuthProvider.mockRejectedValue(new Error('raw provider secret')) const renderer = await renderScreen() await act(async () => { renderer.root.findByProps({ testID: 'sign-up-terms' }).props.onPress() }) await act(async () => { renderer.root.findByProps({ testID: 'signup-oauth-google' }).props.onPress() await Promise.resolve() await Promise.resolve() }) const error = renderer.root.findByProps({ testID: 'auth-error' }) expect(error.findAllByProps({ children: 'mobile.auth.oauthFailed' }).length).toBeGreaterThan(0) expect(error.findAllByProps({ children: 'raw provider secret' })).toHaveLength(0) expect(renderer.root.findByProps({ testID: 'sign-up-submit' })).toBeTruthy() }) it('fails closed when capability discovery fails', async () => { mockGetAuthCapabilities.mockReset().mockRejectedValue(new Error('offline')) const renderer = await renderScreen() expect(() => renderer.root.findByProps({ testID: 'signup-oauth-google' })).toThrow() expect(renderer.root.findByProps({ testID: 'sign-up-submit' })).toBeTruthy() }) it('never starts Google account creation before explicit terms acceptance', async () => { const renderer = await renderScreen() const button = renderer.root.findByProps({ testID: 'signup-oauth-google' }) expect(button.props.disabled).toBe(true) await act(async () => { button.props.onPress() await Promise.resolve() }) expect(mockSignInWithOAuthProvider).not.toHaveBeenCalled() expect(renderer.root.findByProps({ testID: 'auth-error' })).toBeTruthy() }) it('opens both legal documents without accepting the terms on the user\'s behalf', async () => { const renderer = await renderScreen() await act(async () => { renderer.root.findByProps({ testID: 'sign-up-terms-link' }).props.onPress() await Promise.resolve() }) await act(async () => { renderer.root.findByProps({ testID: 'sign-up-privacy-link' }).props.onPress() await Promise.resolve() }) expect(Linking.openURL).toHaveBeenNthCalledWith(1, 'https://d3ro.chanpaca.net/terms/') expect(Linking.openURL).toHaveBeenNthCalledWith(2, 'https://d3ro.chanpaca.net/privacy/') expect(renderer.root.findByProps({ testID: 'sign-up-terms' }).props.accessibilityState) .toEqual({ checked: false, disabled: false }) expect(renderer.root.findByProps({ testID: 'signup-oauth-google' }).props.disabled).toBe(true) }) })