test running channel detection and silent re-checks

Mocks previously set a top-level channel on currentlyRunning, which
masked the build-constant behaviour. Model the build constant and the
manifest metadata channel separately instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-30 22:50:28 +03:00
parent 384987262b
commit 87ee346f79
+143 -10
View File
@@ -4,11 +4,13 @@ import {
fetchUpdateAsync,
reloadAsync,
setExtraParamAsync,
UpdateCheckResultNotAvailableReason,
useUpdates,
} from 'expo-updates'
import {act, renderHook, waitFor} from '@testing-library/react-native'
import {logger} from '#/logger'
import {APP_VERSION} from '#/env'
import {device} from '#/storage'
import {
useApplyPullRequestOTAUpdate,
@@ -22,6 +24,7 @@ jest.mock('expo-updates', () => ({
reloadAsync: jest.fn(),
setExtraParamAsync: jest.fn(),
UpdateCheckResultNotAvailableReason: {
NO_UPDATE_AVAILABLE_ON_SERVER: 'noUpdateAvailableOnServer',
UPDATE_PREVIOUSLY_FAILED: 'updatePreviouslyFailed',
},
useUpdates: jest.fn(),
@@ -42,25 +45,156 @@ jest.mock('#/storage', () => ({
},
}))
const currentUpdate = {
channel: 'testflight',
emergencyLaunchReason: null,
isEmbeddedLaunch: false,
isEmergencyLaunch: false,
updateId: 'current-update',
/**
* `channel` here is the build-time constant baked into the native build, not the
* channel of the running bundle. `channel` is passed as the manifest metadata
* channel our update server stamps into every published update - omit it to
* simulate an embedded launch, which has no server manifest.
*/
function mockCurrentlyRunning({
buildChannel = 'testflight',
channel,
updateId = 'current-update',
}: {
buildChannel?: string
channel?: string
updateId?: string
} = {}) {
const currentlyRunning = {
channel: buildChannel,
emergencyLaunchReason: null,
isEmbeddedLaunch: !channel,
isEmergencyLaunch: false,
updateId,
manifest: channel ? {id: updateId, metadata: {channel}} : undefined,
}
jest.mocked(useUpdates).mockReturnValue({
currentlyRunning,
} as ReturnType<typeof useUpdates>)
return currentlyRunning
}
const currentUpdate = {updateId: 'current-update'}
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(useUpdates).mockReturnValue({
currentlyRunning: currentUpdate,
} as ReturnType<typeof useUpdates>)
mockCurrentlyRunning()
jest.mocked(setExtraParamAsync).mockResolvedValue(undefined)
jest.mocked(reloadAsync).mockResolvedValue(undefined)
jest.spyOn(Alert, 'alert').mockImplementation(() => {})
})
describe('useApplyPullRequestOTAUpdate', () => {
it('detects a running PR deployment from the manifest metadata', () => {
mockCurrentlyRunning({
buildChannel: 'testflight',
channel: 'pull-request-123',
})
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
expect(result.current.currentChannel).toBe('pull-request-123')
expect(result.current.isCurrentlyRunningPullRequestDeployment).toBe(true)
expect(result.current.isCurrentlyRunningNonStandardChannel).toBe(true)
})
it('treats a standard downloaded update as a standard channel', () => {
mockCurrentlyRunning({buildChannel: 'testflight', channel: 'testflight'})
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
expect(result.current.currentChannel).toBe('testflight')
expect(result.current.isCurrentlyRunningPullRequestDeployment).toBe(false)
expect(result.current.isCurrentlyRunningNonStandardChannel).toBe(false)
})
it('falls back to the build channel for an embedded launch', () => {
mockCurrentlyRunning({buildChannel: 'testflight'})
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
expect(result.current.currentChannel).toBe('testflight')
expect(result.current.isCurrentlyRunningNonStandardChannel).toBe(false)
})
it('reports no channel when updates are disabled', () => {
mockCurrentlyRunning({buildChannel: ''})
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
expect(result.current.currentChannel).toBeUndefined()
expect(result.current.isCurrentlyRunningNonStandardChannel).toBe(false)
})
it('stays quiet when already running the latest of the requested channel', async () => {
mockCurrentlyRunning({
buildChannel: 'testflight',
channel: 'pull-request-123',
})
jest.mocked(checkForUpdateAsync).mockResolvedValue({
isAvailable: false,
reason: UpdateCheckResultNotAvailableReason.NO_UPDATE_AVAILABLE_ON_SERVER,
} as Awaited<ReturnType<typeof checkForUpdateAsync>>)
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
await act(() => result.current.tryApplyUpdate('pull-request-123'))
expect(Alert.alert).not.toHaveBeenCalled()
})
it('warns when no deployment is available for a different channel', async () => {
mockCurrentlyRunning({
buildChannel: 'testflight',
channel: 'pull-request-123',
})
jest.mocked(checkForUpdateAsync).mockResolvedValue({
isAvailable: false,
reason: UpdateCheckResultNotAvailableReason.NO_UPDATE_AVAILABLE_ON_SERVER,
} as Awaited<ReturnType<typeof checkForUpdateAsync>>)
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
await act(() => result.current.tryApplyUpdate('pull-request-456'))
expect(Alert.alert).toHaveBeenCalledWith(
'No Deployment Available',
expect.stringContaining('pull-request-456'),
)
})
it('stays silent on a re-fired intent even when the app version differs', async () => {
mockCurrentlyRunning({
buildChannel: 'testflight',
channel: 'pull-request-123',
})
jest.mocked(checkForUpdateAsync).mockResolvedValue({
isAvailable: false,
reason: UpdateCheckResultNotAvailableReason.NO_UPDATE_AVAILABLE_ON_SERVER,
} as Awaited<ReturnType<typeof checkForUpdateAsync>>)
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
await act(() => result.current.tryApplyUpdate('pull-request-123', '0.0.0'))
expect(Alert.alert).not.toHaveBeenCalled()
expect(fetchUpdateAsync).not.toHaveBeenCalled()
})
it('prompts to apply an available update when the app version matches', async () => {
jest.mocked(checkForUpdateAsync).mockResolvedValue({
isAvailable: true,
} as Awaited<ReturnType<typeof checkForUpdateAsync>>)
const {result} = renderHook(() => useApplyPullRequestOTAUpdate())
await act(() =>
result.current.tryApplyUpdate('pull-request-123', APP_VERSION),
)
expect(Alert.alert).toHaveBeenCalledWith(
'Apply update from PR #123?',
expect.stringContaining('relaunch'),
expect.arrayContaining([expect.objectContaining({text: 'Apply'})]),
)
})
it('warns before applying an OTA built for a different app version', async () => {
jest.mocked(checkForUpdateAsync).mockResolvedValue({
isAvailable: true,
@@ -74,7 +208,6 @@ describe('useApplyPullRequestOTAUpdate', () => {
await act(() => result.current.tryApplyUpdate('pull-request-123', '0.0.0'))
expect(checkForUpdateAsync).not.toHaveBeenCalled()
expect(Alert.alert).toHaveBeenCalledWith(
'App Version Mismatch',
expect.stringContaining('Applying it anyway may cause'),