import React from 'react' import ReactTestRenderer, { act } from 'react-test-renderer' jest.mock('../src/lib/auth-context', () => ({ useAuth: jest.fn() })) jest.mock('../src/lib/entitlement-context', () => ({ useEntitlement: jest.fn() })) jest.mock('../src/lib/native-config', () => ({ getMobileRuntimeConfig: jest.fn() })) jest.mock('../src/lib/supabase', () => ({ supabase: { from: jest.fn() } })) jest.mock('../src/lib/preferences-context', () => ({ useMobilePreferences: jest.fn(() => ({ palette: { accent: { main: '#3366ff' }, bg: { inset: '#111111' }, border: { subtle: '#222222' }, tag: { green: '#00aa66', red: '#cc3344' }, text: { primary: '#ffffff', secondary: '#dddddd', muted: '#999999', onAccent: '#ffffff', }, }, })), })) const mockedAuth = jest.requireMock('../src/lib/auth-context') as { useAuth: jest.Mock } const mockedEntitlement = jest.requireMock('../src/lib/entitlement-context') as { useEntitlement: jest.Mock } const mockedConfig = jest.requireMock('../src/lib/native-config') as { getMobileRuntimeConfig: jest.Mock } // The mapper already resolves this import to our manual mock. requireActual // keeps the same module instance that production code receives. const mockedAds = jest.requireActual('react-native-google-mobile-ads') as { AdsConsent: { gatherConsent: jest.Mock getConsentInfo: jest.Mock } MobileAds: jest.Mock BannerAd: jest.Mock useRewardedAd: jest.Mock } import { MobileAdsProvider, useRewardedCredits } from '../src/lib/mobile-ads-context' import FreeTierBanner from '../src/components/FreeTierBanner' const USER_ID = '11111111-1111-4111-8111-111111111111' const REWARDED_TEST_ID = 'ca-app-pub-3940256099942544/5224354917' describe('mobile rewarded ad lifecycle', () => { beforeEach(() => { mockedAuth.useAuth.mockReturnValue({ user: { id: USER_ID } }) mockedEntitlement.useEntitlement.mockReturnValue({ loading: false, stale: false, error: null, snapshot: { tier: 'free', adFree: false, overageCredits: 0, }, refresh: jest.fn(async () => null), }) mockedConfig.getMobileRuntimeConfig.mockReturnValue({ debug: true, adMobBannerUnitId: 'ca-app-pub-3940256099942544/6300978111', adMobRewardedUnitId: REWARDED_TEST_ID, }) const consentInfo = { canRequestAds: true, privacyOptionsRequirementStatus: 'NOT_REQUIRED', } mockedAds.AdsConsent.gatherConsent.mockReset().mockResolvedValue(consentInfo) mockedAds.AdsConsent.getConsentInfo.mockReset().mockResolvedValue(consentInfo) mockedAds.MobileAds.mockClear() mockedAds.BannerAd.mockClear() mockedAds.BannerAd.mockImplementation(() => React.createElement(React.Fragment, null)) }) it('retries loading when the native hook replaces its initial null-ad callback', async () => { const nullAdLoad = jest.fn() const nativeAdLoad = jest.fn() mockedAds.useRewardedAd.mockImplementation((adUnitId: string | null) => { const [nativeAdReady, setNativeAdReady] = React.useState(false) React.useEffect(() => { const timer = setTimeout(() => { setNativeAdReady(adUnitId !== null) }, 0) return () => clearTimeout(timer) }, [adUnitId]) const load = React.useCallback(() => { if (nativeAdReady) nativeAdLoad() else nullAdLoad() }, [nativeAdReady]) return { error: undefined, isClosed: false, isEarnedReward: false, isLoaded: false, isShowing: false, load, show: jest.fn(), } }) function Probe(): null { useRewardedCredits() return null } let renderer: ReactTestRenderer.ReactTestRenderer await act(async () => { renderer = ReactTestRenderer.create( , ) await Promise.resolve() await Promise.resolve() }) for (let index = 0; index < 5; index += 1) { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)) }) } expect(mockedAds.AdsConsent.gatherConsent).toHaveBeenCalledTimes(1) expect(mockedAds.useRewardedAd).toHaveBeenCalledWith( REWARDED_TEST_ID, expect.objectContaining({ requestNonPersonalizedAdsOnly: true }), ) expect(nullAdLoad).toHaveBeenCalledTimes(1) expect(nativeAdLoad).toHaveBeenCalledTimes(1) act(() => renderer!.unmount()) }) it('remounts a banner with bounded backoff after a transient load failure', async () => { jest.useFakeTimers() let latestBannerProps: { onAdFailedToLoad: () => void onAdLoaded: () => void unitId: string } | null = null mockedAds.BannerAd.mockImplementation((props) => { latestBannerProps = props return React.createElement(React.Fragment, null) }) let renderer: ReactTestRenderer.ReactTestRenderer await act(async () => { renderer = ReactTestRenderer.create( , ) await Promise.resolve() await Promise.resolve() }) expect(latestBannerProps?.unitId).toBe('ca-app-pub-3940256099942544/6300978111') act(() => latestBannerProps?.onAdFailedToLoad()) expect(JSON.stringify(renderer!.toJSON())).toContain('AD UNAVAILABLE') await act(async () => { jest.advanceTimersByTime(30_000) await Promise.resolve() }) expect(mockedAds.BannerAd).toHaveBeenCalledTimes(2) expect(latestBannerProps?.unitId).toBe('ca-app-pub-3940256099942544/6300978111') act(() => latestBannerProps?.onAdLoaded()) expect(JSON.stringify(renderer!.toJSON())).not.toContain('AD UNAVAILABLE') act(() => renderer!.unmount()) jest.useRealTimers() }) })