d3ro-voice/apps/mobile-rn/__tests__/mobile-ads.test.tsx
Yun Chan 5ca3709b32
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
feat: add mobile shell and unified search
2026-09-28 23:23:36 +09:00

167 lines
5.7 KiB
TypeScript

import React from 'react'
import ReactTestRenderer, { act } from 'react-test-renderer'
import { I18nProvider } from '@d3ro/i18n'
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', () => {
const { d3roNativeDarkPalette } = jest.requireActual('@d3ro/ui-native') as typeof import('@d3ro/ui-native')
return {
useMobilePreferences: jest.fn(() => ({ palette: d3roNativeDarkPalette })),
}
})
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(
<MobileAdsProvider><Probe /></MobileAdsProvider>,
)
await Promise.resolve()
await Promise.resolve()
})
for (let index = 0; index < 5; index += 1) {
await act(async () => {
await new Promise<void>((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(
<I18nProvider initialLocale="en">
<MobileAdsProvider><FreeTierBanner /></MobileAdsProvider>
</I18nProvider>,
)
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()
})
})