feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
174
apps/mobile-rn/__tests__/mobile-ads.test.tsx
Normal file
174
apps/mobile-rn/__tests__/mobile-ads.test.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
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(
|
||||
<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(
|
||||
<MobileAdsProvider><FreeTierBanner /></MobileAdsProvider>,
|
||||
)
|
||||
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()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue