Files
orca/mobile/src/components/ProtocolBlockScreen.test.ts
T
Jinwoo Hong b2419a58ad fix(mobile): show the update screen instead of native screens when the desktop has no page (#29956)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test
2026-10-17 02:01:20 -07:00

243 lines
9.3 KiB
TypeScript

import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { KnownAppUpdate } from '../storage/app-update-preferences'
import type { BlockedVerdict } from './ProtocolBlockScreen'
import { ProtocolBlockScreen } from './ProtocolBlockScreen'
const nativeTestState = vi.hoisted(() => {
// Declared wide so a test can switch stores; an assertion here would only widen the same literal.
const platform: { OS: 'ios' | 'android' } = { OS: 'ios' }
return { openUrl: vi.fn(), platform }
})
const wallUpdate = vi.hoisted(() => {
const state: { current: KnownAppUpdate | null } = { current: null }
return state
})
// The release the installed app's update check found; the real hook imports the checker module.
vi.mock('../app-update/use-wall-app-update', () => ({ useWallAppUpdate: () => wallUpdate.current }))
vi.mock('react-native', () => ({
Linking: { openURL: nativeTestState.openUrl },
Platform: nativeTestState.platform,
Pressable: 'Pressable',
StyleSheet: { create: <T>(styles: T) => styles },
Text: 'Text',
View: 'View'
}))
vi.mock('expo-router', () => ({
router: { replace: vi.fn() },
// `ProtocolBlockScreen` reaches the router through the navigation handoff now, and the handoff's
// native form is this hook. Its web form is what posts the target to the shell.
useRouter: () => ({ replace: vi.fn(), push: vi.fn(), back: vi.fn(), dismissTo: vi.fn() })
}))
const RELEASES_URL = 'https://github.com/stablyai/orca/releases'
let renderer: ReactTestRenderer | null = null
function render(verdict: BlockedVerdict, mobileUpdate: KnownAppUpdate | null = null): string {
wallUpdate.current = mobileUpdate
act(() => {
renderer = create(createElement(ProtocolBlockScreen, { verdict }))
})
return JSON.stringify(renderer?.toJSON())
}
/** The mocked host components are plain strings, which `ElementType` does not admit. */
function isMockedHostElement(type: unknown, name: string): boolean {
return type === name
}
function pressableCount(): number {
return renderer?.root.findAll((node) => isMockedHostElement(node.type, 'Pressable')).length ?? 0
}
function primaryActionUrl(): unknown {
const pressable = renderer?.root.findAll((node) => isMockedHostElement(node.type, 'Pressable'))[0]
act(() => pressable?.props.onPress())
return nativeTestState.openUrl.mock.calls[0]?.[0]
}
describe('ProtocolBlockScreen', () => {
beforeEach(() => {
nativeTestState.openUrl.mockClear()
nativeTestState.platform.OS = 'ios'
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
})
// Why: the protocol wall shipped before the bundle one; its copy is what users already see.
it('keeps the existing protocol wall rendering unchanged', () => {
const mobile = render({
kind: 'blocked',
reason: 'mobile-too-old',
desktopVersion: 5,
requiredMobileVersion: 99
})
expect(mobile).toContain('Update Orca Mobile')
expect(mobile).toContain(
'This desktop needs a newer Orca Mobile app. Update Orca Mobile from the App Store, then try this host again.'
)
expect(mobile).toContain('Open App Store')
act(() => renderer?.unmount())
const desktop = render({
kind: 'blocked',
reason: 'desktop-too-old',
desktopVersion: 0,
requiredDesktopVersion: 2
})
expect(desktop).toContain('Update Orca on your computer')
expect(desktop).toContain(
'This paired desktop app is too old for your current Orca Mobile app. Update Orca on your computer, then try this host again.'
)
expect(desktop).toContain('Open GitHub Releases')
})
it('tells a host without a bundle, or without this screen, the desktop is too old', () => {
for (const reason of ['bundle-unavailable', 'page-route-unlisted'] as const) {
const output = render({ kind: 'blocked', reason })
expect(output).toContain('Update Orca on your computer')
expect(output).toContain(
'This paired desktop app is too old for your current Orca Mobile app. Update Orca on your computer, then try this host again.'
)
expect(output).not.toContain('does not include the mobile workspace')
expect(primaryActionUrl()).toBe(RELEASES_URL)
}
})
it('sends a screen that needs a grant this app lacks to the mobile update', () => {
const output = render({ kind: 'blocked', reason: 'page-route-grant-missing' })
expect(output).toContain('Update Orca Mobile')
expect(output).toContain("This desktop's mobile workspace needs a newer Orca Mobile app.")
})
it('sends an unknown manifest schema to the mobile update', () => {
const output = render({
kind: 'blocked',
reason: 'bundle-shell-too-old',
schemaVersion: 2
})
expect(output).toContain('Update Orca Mobile')
expect(output).toContain(
"This desktop's mobile workspace needs a newer Orca Mobile app. Update Orca Mobile from the App Store, then try this host again."
)
expect(primaryActionUrl()).toBe('itms-apps://apps.apple.com/app/orca-ide/id6766130217')
})
it('offers no download for a cached bundle the host outgrew, because none would clear it', () => {
const output = render({
kind: 'blocked',
reason: 'bundle-incompatible',
side: 'mobile',
bundleRuntimeProtocolVersion: 3,
requiredBundleRuntimeProtocolVersion: 4
})
expect(output).toContain('Refresh the mobile workspace')
expect(output).toContain(
'The workspace cached for this host is older than the desktop expects. Reconnect to this host to download the current one.'
)
// A store update cannot replace a stale cache, so neither store link is offered.
expect(output).not.toContain('Open App Store')
expect(output).not.toContain('Open GitHub Releases')
expect(output).not.toContain('Update Orca')
// Back to hosts is the only button left, and it is not a download.
expect(pressableCount()).toBe(1)
expect(output).toContain('Back to hosts')
// Nothing was "already updated" here; the note keeps only the pairing fallback.
expect(output).not.toContain('Already updated?')
expect(output).toContain('If this message stays, remove this host and pair it again.')
})
it('sends a host older than its own bundle to the desktop update', () => {
const output = render({
kind: 'blocked',
reason: 'bundle-incompatible',
side: 'desktop',
hostProtocolVersion: 1,
requiredHostProtocolVersion: 2
})
expect(output).toContain('Update Orca on your computer')
expect(output).toContain('This paired desktop app is too old for your current Orca Mobile app')
expect(primaryActionUrl()).toBe(RELEASES_URL)
})
it('sends a desktop whose page is older than this shell to the desktop update', () => {
const output = render({
kind: 'blocked',
reason: 'bundle-incompatible',
side: 'desktop',
pageVersion: 0,
requiredPageVersion: 1
})
expect(output).toContain('Update Orca on your computer')
expect(output).toContain('This paired desktop app is too old for your current Orca Mobile app')
expect(primaryActionUrl()).toBe(RELEASES_URL)
})
it('routes an Android bundle wall to GitHub Releases, not a store that has no listing', () => {
nativeTestState.platform.OS = 'android'
const output = render({
kind: 'blocked',
reason: 'bundle-shell-too-old',
schemaVersion: 2
})
expect(output).toContain('Update Orca Mobile from GitHub Releases')
expect(primaryActionUrl()).toBe(RELEASES_URL)
})
it('keeps the update walls on two buttons and the full recovery note', () => {
const output = render({ kind: 'blocked', reason: 'bundle-unavailable' })
expect(output).toContain('Already updated? Go back to Hosts and refresh the connection.')
// The presence precondition for the absence asserted on the refresh wall above.
expect(pressableCount()).toBe(2)
})
describe('with the newest release known', () => {
const release = {
version: '0.0.52',
url: 'https://github.com/stablyai/orca/releases/tag/mobile-v0.0.52'
}
it.each(['ios', 'android'] as const)('opens that exact release on %s', (os) => {
nativeTestState.platform.OS = os
const output = render(
{ kind: 'blocked', reason: 'bundle-shell-too-old', schemaVersion: 2 },
release
)
expect(output).toContain('Get Orca 0.0.52')
expect(output).not.toContain('Open App Store')
expect(output).not.toContain('Open GitHub Releases')
expect(primaryActionUrl()).toBe(release.url)
})
it('leaves the desktop wall on the desktop releases', () => {
const output = render({ kind: 'blocked', reason: 'bundle-unavailable' }, release)
expect(output).not.toContain('Get Orca')
expect(primaryActionUrl()).toBe(RELEASES_URL)
})
})
it.each([
['ios', 'Open App Store', 'itms-apps://apps.apple.com/app/orca-ide/id6766130217'],
['android', 'Open GitHub Releases', RELEASES_URL]
] as const)('keeps the %s store link when no release is known', (os, label, url) => {
nativeTestState.platform.OS = os
const output = render(
{ kind: 'blocked', reason: 'mobile-too-old', desktopVersion: 5, requiredMobileVersion: 9 },
null
)
expect(output).toContain(label)
expect(output).not.toContain('Get Orca')
expect(primaryActionUrl()).toBe(url)
})
})