mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
test(mobile): the page's pane hook gets its own test (OTA phase C, C7.7 round 2)
pullfrog: `use-notification-pane-navigation.web.ts` had no cover. The native file's test mounts the native file, and `bridge-route-update.test.ts` stops at the client, so the half that turns a route update into a tab switch was untested. Seven cases: the seed from the route the page was opened on, a request held until the terminals load, a repeat tap on the pane already showing, a different pane, the clear the shell posts after each delivery, a pane that has since closed, and a page opened on no pane at all. Two controls, so the cases are not all satisfied by one behaviour. Dropping the seed reds the two that read the first `init`. Deduplicating by value instead of counting deliveries reds the repeat tap, which is the case the counter exists for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { BridgeInitRoute } from '../mobile-web-shell/bridge/bridge-envelope'
|
||||
import type { MobileSessionTab } from './mobile-session-route-types'
|
||||
|
||||
type RouteUpdateListener = (route: BridgeInitRoute | null) => void
|
||||
|
||||
const bridge = vi.hoisted(() => {
|
||||
const held: {
|
||||
route: BridgeInitRoute | null
|
||||
listeners: RouteUpdateListener[]
|
||||
} = { route: null, listeners: [] }
|
||||
return held
|
||||
})
|
||||
|
||||
vi.mock('../transport/client-context.web', () => ({
|
||||
usePageBridgeClient: () => ({
|
||||
getShellSession: () => ({ route: bridge.route }),
|
||||
onRouteUpdate: (listener: RouteUpdateListener) => {
|
||||
bridge.listeners.push(listener)
|
||||
return () => {
|
||||
bridge.listeners = bridge.listeners.filter((held) => held !== listener)
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
import { useNotificationPaneNavigation } from './use-notification-pane-navigation.web'
|
||||
|
||||
/** A real leaf id: `parsePaneKey` refuses anything that is not one, so a made-up key parses to
|
||||
* nothing and every case below would read as "the pane closed". */
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
const TABS: MobileSessionTab[] = [
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'first',
|
||||
parentTabId: 'tab-a',
|
||||
leafId: LEAF,
|
||||
title: 'first',
|
||||
terminal: 'pty-a',
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'second',
|
||||
parentTabId: 'tab-b',
|
||||
leafId: LEAF,
|
||||
title: 'second',
|
||||
terminal: 'pty-b',
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
|
||||
const switched: MobileSessionTab[] = []
|
||||
|
||||
function Probe({ terminalsLoaded }: { terminalsLoaded: boolean }): null {
|
||||
useNotificationPaneNavigation({
|
||||
sessionTabs: TABS,
|
||||
terminalsLoaded,
|
||||
switchSessionTab: (tab) => switched.push(tab)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
function render(terminalsLoaded: boolean): ReactTestRenderer {
|
||||
const rendered: { tree: ReactTestRenderer | null } = { tree: null }
|
||||
act(() => {
|
||||
rendered.tree = create(createElement(Probe, { terminalsLoaded }))
|
||||
})
|
||||
if (rendered.tree === null) {
|
||||
throw new Error('the probe did not render')
|
||||
}
|
||||
return rendered.tree
|
||||
}
|
||||
|
||||
/** What the shell does: re-send `init`, which the client publishes as a route that moved. */
|
||||
function deliver(paneKey: string): void {
|
||||
act(() => {
|
||||
for (const listener of bridge.listeners.slice()) {
|
||||
listener({ pathname: '/h/host-1/session/wt-1', params: { paneKey } })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
bridge.route = null
|
||||
bridge.listeners = []
|
||||
switched.length = 0
|
||||
})
|
||||
|
||||
/**
|
||||
* The page's pane hook, which has no route to read and no param to write back.
|
||||
*
|
||||
* Its native sibling reads `paneKey` off the app's route and clears it with `setParams`; inside
|
||||
* the page the document is served at `/` with one history entry, so the request arrives as a
|
||||
* re-sent `init` instead (ruling 33.1) and the shell clears the native param once this page has
|
||||
* been handed one. The native file's test mounts the native file, and the bridge test stops at
|
||||
* the client, so this is the only cover this half has.
|
||||
*/
|
||||
describe('the page pane hook', () => {
|
||||
it('switches to the pane the page was opened on, which arrives in the first init', () => {
|
||||
bridge.route = { pathname: '/h/host-1/session/wt-1', params: { paneKey: `tab-b:${LEAF}` } }
|
||||
render(true)
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['second'])
|
||||
})
|
||||
|
||||
it('holds that request until the terminals have loaded, rather than dropping it', () => {
|
||||
bridge.route = { pathname: '/h/host-1/session/wt-1', params: { paneKey: `tab-b:${LEAF}` } }
|
||||
const tree = render(false)
|
||||
expect(switched).toEqual([])
|
||||
act(() => {
|
||||
tree.update(createElement(Probe, { terminalsLoaded: true }))
|
||||
})
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['second'])
|
||||
})
|
||||
|
||||
it('switches again for a repeat tap on the pane already showing', () => {
|
||||
// The counter is why: the route is the one this hook last acted on, so a listener that
|
||||
// compared values would read the second tap as nothing having changed.
|
||||
render(true)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['first', 'first'])
|
||||
})
|
||||
|
||||
it('takes a different pane as its own request', () => {
|
||||
render(true)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
deliver(`tab-b:${LEAF}`)
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['first', 'second'])
|
||||
})
|
||||
|
||||
it('ignores the clear the shell posts after each delivery', () => {
|
||||
// Load-bearing rather than defensive: the shell clears the native param once the page has the
|
||||
// route, and that clear is itself a route that moved.
|
||||
render(true)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
deliver('')
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['first'])
|
||||
})
|
||||
|
||||
it('consumes a request for a pane that has since closed, so no later render serves it', () => {
|
||||
render(true)
|
||||
deliver(`gone:${LEAF}`)
|
||||
expect(switched).toEqual([])
|
||||
// A re-render with nothing new delivered must not go looking for it again.
|
||||
deliver('')
|
||||
expect(switched).toEqual([])
|
||||
})
|
||||
|
||||
it('does nothing at all when the page was opened on no pane', () => {
|
||||
render(true)
|
||||
expect(switched).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user