Files
orca/src/main/runtime/browser-session-tab-selection-snapshot.test.ts
T
Neil b497f15b53 fix: preserve renderer browser publication during client-hosted page updates (#18961)
* fix: preserve renderer browser publication during client-hosted page updates

* refactor: drop the now-dead publicationEpoch selection argument

applyBrowserSessionTabSelection took a publicationEpoch and wrote it over
the epoch the spread snapshot already carried. Its only production caller
now passes snapshot.publicationEpoch, so the parameter is a no-op whose
only remaining power is to reintroduce the epoch rotation this PR fixes.

Remove it, and collapse the repeated prototype-cast boilerplate in the new
reconciliation test into one helper.

No behavior change.

* fix: keep reconcile from publishing a browser row twice

The retention filter partitioned existing rows by placement kind, so its
disjointness from the live build relied on a non-local invariant: that the
page registry only ever stores client placements and that server tabs are
empty while no offscreen backend exists. Drop ids the live build already
published instead, so a duplicate row is impossible by construction rather
than by coincidence.

* fix: stop the browser reconcile republishing on a pure reordering

headlessBrowserTabsUnchanged compares by array index, so rebuilding the live
list renderer-first read an interleaved snapshot as changed and republished
with a bumped version and rebuilt tab groups for no semantic change - the
same churn this branch exists to remove.

Key the live set by id and emit it in the order the snapshot already had.
Keying also makes uniqueness unconditional rather than resting on the page
registry only ever storing client placements.
2026-09-06 18:29:22 -07:00

124 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types'
import { applyBrowserSessionTabSelection } from './browser-session-tab-selection-snapshot'
function makeSnapshot(): RuntimeMobileSessionTabsSnapshot {
return {
worktree: 'wt-1',
publicationEpoch: 'headless:before',
snapshotVersion: 4,
activeGroupId: 'group-left',
activeTabId: 'terminal-tab',
activeTabType: 'terminal',
tabs: [
{
type: 'terminal',
id: 'terminal-tab',
parentTabId: 'terminal-tab',
leafId: 'leaf-1',
title: 'Terminal',
isActive: true
},
{
type: 'browser',
id: 'page-new',
browserWorkspaceId: 'page-new',
browserPageId: 'page-new',
title: 'New',
url: 'about:blank',
loading: false,
canGoBack: false,
canGoForward: false,
isActive: false
}
] as RuntimeMobileSessionTabsSnapshot['tabs'],
tabGroups: [
{ id: 'group-left', tabOrder: ['terminal-tab', 'page-new'], activeTabId: 'terminal-tab' },
{ id: 'group-right', tabOrder: [], activeTabId: null }
] as RuntimeMobileSessionTabsSnapshot['tabGroups']
}
}
function select(overrides: { focusesHost: boolean; targetGroupId?: string }) {
return applyBrowserSessionTabSelection({
snapshot: makeSnapshot(),
tabId: 'page-new',
focusesHost: overrides.focusesHost,
...(overrides.targetGroupId ? { targetGroupId: overrides.targetGroupId } : {})
})
}
describe('applyBrowserSessionTabSelection', () => {
it('moves the shared active tab when the request reaches the host', () => {
const { snapshot } = select({ focusesHost: true })
expect(snapshot.activeTabId).toBe('page-new')
expect(snapshot.activeTabType).toBe('browser')
expect(snapshot.tabs.find((tab) => tab.id === 'page-new')?.isActive).toBe(true)
})
it('leaves every shared selection alone for a caller-addressed create', () => {
const before = makeSnapshot()
const { snapshot } = select({ focusesHost: false })
expect(snapshot.activeTabId).toBe(before.activeTabId)
expect(snapshot.activeTabType).toBe(before.activeTabType)
expect(snapshot.activeGroupId).toBe(before.activeGroupId)
expect(snapshot.tabs.find((tab) => tab.id === 'page-new')?.isActive).toBe(false)
expect(snapshot.tabGroups?.find((group) => group.id === 'group-left')?.activeTabId).toBe(
'terminal-tab'
)
})
it('places the tab in the requested group whether or not the host follows', () => {
for (const focusesHost of [true, false]) {
const { snapshot, placedInTargetGroup } = select({
focusesHost,
targetGroupId: 'group-right'
})
expect(placedInTargetGroup).toBe(true)
expect(snapshot.tabGroups?.find((group) => group.id === 'group-right')?.tabOrder).toEqual([
'page-new'
])
expect(snapshot.tabGroups?.find((group) => group.id === 'group-left')?.tabOrder).toEqual([
'terminal-tab'
])
}
})
it('claims the target group only for a host-following create', () => {
expect(select({ focusesHost: true, targetGroupId: 'group-right' }).snapshot).toMatchObject({
activeGroupId: 'group-right',
tabGroups: expect.arrayContaining([
expect.objectContaining({ id: 'group-right', activeTabId: 'page-new' })
])
})
expect(select({ focusesHost: false, targetGroupId: 'group-right' }).snapshot).toMatchObject({
activeGroupId: 'group-left',
tabGroups: expect.arrayContaining([
expect.objectContaining({ id: 'group-right', activeTabId: null })
])
})
})
it('reports an unknown group as unplaced so nothing is persisted for it', () => {
const { placedInTargetGroup, snapshot } = select({
focusesHost: true,
targetGroupId: 'group-missing'
})
expect(placedInTargetGroup).toBe(false)
expect(snapshot.activeGroupId).toBe('group-left')
})
// Rotating the epoch here retires the renderer's own publication client-side.
it('keeps the publication epoch and advances the version either way', () => {
for (const focusesHost of [true, false]) {
const { snapshot } = select({ focusesHost })
expect(snapshot.publicationEpoch).toBe('headless:before')
expect(snapshot.snapshotVersion).toBe(5)
}
})
})