Files
orca/config/scripts/mobile-web-app/mobile-web-app-stack-transition-render.test.mjs
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

368 lines
15 KiB
JavaScript

import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { chromium, webkit } from 'playwright-core'
import { buildMobileWebAppBundle } from '../build-mobile-web-app-bundle.mjs'
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
import { MOBILE_WEB_APP_ROUTE_ROOT } from './mobile-web-app-route-manifest.mjs'
import {
createBundleServer,
installShellDouble,
readBridgeBackNames,
readBridgeFaultGrant,
readBridgeProtocolVersion,
readShellCsp
} from './preview/mobile-web-app-render-harness.mjs'
// Sample real HostStack web push/pop frame-by-frame; native-stack web fallback only flips display.
// Two marked probe screens isolate stack motion from screen content while extensionless import selects shipped web sibling.
const HOST_ID = 'stack-host'
const LIST_ROUTE = `/${MOBILE_WEB_APP_ROUTE_ROOT}/${HOST_ID}`
const SESSION_HREF = `${LIST_ROUTE}/session/wt-1`
const VIEWPORT = { width: 390, height: 844 }
const SAMPLE_MS = 1500
const SETTLED_GRACE_MS = 150
const bundles = mobileWebAppDependenciesPresent()
const describeRender = bundles ? describe : describe.skip
// Include WebKit Web Animations/keyframes used by iOS WKWebView.
const ENGINES = [
{
name: 'chromium',
launch: () => {
const executablePath = process.env.ORCA_MOBILE_WEB_RENDER_BROWSER
return chromium.launch({ headless: true, ...(executablePath ? { executablePath } : {}) })
}
},
{ name: 'webkit', launch: () => webkit.launch({ headless: true }) }
]
const browsers = new Map()
let origin = null
let scratch = null
let server = null
let bridgeVersion = null
let faultGrant = null
let backNames = null
const layoutSource = (
hostStackModule
) => `import { HostStack } from ${JSON.stringify(hostStackModule)}
export default function ProbeHostLayout() {
return <HostStack animation={globalThis.__orcaStackProbeAnimation ?? 'default'} />
}
`
// Use real page router seam so Back claims/pops match shipped behavior.
const listSource = (handoffModule) => `import { useEffect } from 'react'
import { View } from 'react-native'
import { useRouteHandoff } from ${JSON.stringify(handoffModule)}
export default function ProbeList() {
const router = useRouteHandoff()
useEffect(() => {
globalThis.__orcaStackProbe = {
push: () => router.push(${JSON.stringify(SESSION_HREF)}),
pushOther: () => router.push(${JSON.stringify(`${LIST_ROUTE}/session/wt-2`)}),
back: () => router.back(),
hardwareBack: () => globalThis.__orcaRenderCheckSendBack()
}
}, [router])
return <View testID="stack-probe-list" style={{ flex: 1, backgroundColor: '#204060' }} />
}
`
// Count mounts to catch exit animation replacing screen instead of retaining it.
const sessionSource = (handoffModule) => `import { useEffect } from 'react'
import { View } from 'react-native'
import { useRouteHandoff } from ${JSON.stringify(handoffModule)}
export default function ProbeSession() {
useRouteHandoff()
useEffect(() => {
globalThis.__orcaSessionMounts = (globalThis.__orcaSessionMounts ?? 0) + 1
}, [])
return <View testID="stack-probe-session" style={{ flex: 1, backgroundColor: '#602040' }} />
}
`
beforeAll(async () => {
if (!bundles) {
return
}
const projectDir = fileURLToPath(new URL('../../..', import.meta.url))
const cspHeader = await readShellCsp()
bridgeVersion = await readBridgeProtocolVersion()
faultGrant = await readBridgeFaultGrant()
scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-stack-transition-'))
const routeDir = join(scratch, 'app', MOBILE_WEB_APP_ROUTE_ROOT)
await mkdir(join(routeDir, '[hostId]', 'session'), { recursive: true })
const navigationDir = join(projectDir, 'mobile', 'src', 'navigation')
const handoffModule = join(navigationDir, 'route-handoff')
backNames = await readBridgeBackNames()
await writeFile(join(routeDir, '_layout.tsx'), layoutSource(join(navigationDir, 'host-stack')))
await writeFile(join(routeDir, '[hostId]', 'index.tsx'), listSource(handoffModule))
await writeFile(
join(routeDir, '[hostId]', 'session', '[worktreeId].tsx'),
sessionSource(handoffModule)
)
const built = await buildMobileWebAppBundle({
appDir: join(scratch, 'app'),
outDir: join(scratch, 'bundle'),
pageRoutes: [
{ pathname: `/${MOBILE_WEB_APP_ROUTE_ROOT}/[hostId]`, grants: [] },
{ pathname: `/${MOBILE_WEB_APP_ROUTE_ROOT}/[hostId]/session/[worktreeId]`, grants: [] }
]
})
const served = await createBundleServer({ outDir: built.outDir, cspHeader })
server = served.server
origin = served.origin
for (const engine of ENGINES) {
browsers.set(engine.name, await engine.launch())
}
}, 600_000)
afterAll(async () => {
for (const browser of browsers.values()) {
await browser.close()
}
server?.close()
if (scratch) {
await rm(scratch, { recursive: true, force: true })
}
})
async function openList(browser, { animation = 'default', reducedMotion = 'no-preference' } = {}) {
const page = await browser.newPage({ viewport: VIEWPORT, reducedMotion })
await page.addInitScript((value) => {
globalThis.__orcaStackProbeAnimation = value
}, animation)
await page.addInitScript(installShellDouble, {
version: bridgeVersion,
sessionId: 'stack-transition-session',
buildId: 'stack-transition-build',
route: { pathname: LIST_ROUTE, params: {} },
host: { id: HOST_ID, name: 'Stack Host', endpoint: 'ws://stack', lastConnected: 1 },
storage: {},
faultGrant,
backFrame: backNames.frame,
pageRoutes: [
`/${MOBILE_WEB_APP_ROUTE_ROOT}/[hostId]`,
`/${MOBILE_WEB_APP_ROUTE_ROOT}/[hostId]/session/[worktreeId]`
],
replies: {}
})
const errors = []
page.on('pageerror', (error) => errors.push(`${error.name}: ${error.message}`))
await page.goto(`${origin}/`, { waitUntil: 'load' })
await page.waitForFunction(() => globalThis.__orcaStackProbe !== undefined, null, {
timeout: 60_000,
polling: 100
})
return { errors, page }
}
// Sample screen left edges and center hit target each rAF; hidden/missing screens read null.
function sampleFrames(
page,
action,
{ followUp = null, afterFrames = 0, observeFullWindow = false } = {}
) {
return page.evaluate(
([name, sampleMs, nextAction, followAt, settledGraceMs, fullWindow]) =>
new Promise((resolve) => {
const leftOf = (id) => {
const node = document.querySelector(`[data-testid="${id}"]`)
const rect = node?.getBoundingClientRect()
return rect && rect.width > 0 ? Math.round(rect.left) : null
}
const hitAt = () =>
document
.elementFromPoint(innerWidth / 2, innerHeight / 2)
?.closest('[data-testid^="stack-probe-"]')
?.getAttribute('data-testid')
?.replace('stack-probe-', '') ?? null
const frames = []
const start = performance.now()
let settledAt = null
const destination = nextAction ?? name
const pushing = destination === 'push' || destination === 'pushOther'
globalThis.__orcaStackProbe[name]()
const tick = () => {
if (nextAction !== null && frames.length === followAt) {
globalThis.__orcaStackProbe[nextAction]()
}
frames.push({
list: leftOf('stack-probe-list'),
session: leftOf('stack-probe-session'),
hit: hitAt(),
from: document.getAnimations()[0]?.effect?.getKeyframes()[0]?.transform ?? null
})
const last = frames.at(-1)
const arrived = pushing
? last.list === null && last.session === 0 && last.hit === 'session'
: last.list === 0 && last.session === null && last.hit === 'list'
const followUpDelivered = nextAction === null || frames.length > followAt
const settled = arrived && followUpDelivered && document.getAnimations().length === 0
const now = performance.now()
settledAt = settled ? (settledAt ?? now) : null
const finished = !fullWindow && settledAt !== null && now - settledAt >= settledGraceMs
if (!finished && now - start < sampleMs) {
requestAnimationFrame(tick)
} else {
resolve(frames)
}
}
requestAnimationFrame(tick)
}),
[action, SAMPLE_MS, followUp, afterFrames, SETTLED_GRACE_MS, observeFullWindow]
)
}
const nodeCount = (page) => page.evaluate(() => document.querySelectorAll('*').length)
async function waitForBackClaim(page) {
await page.waitForFunction(
(name) =>
globalThis.__orcaRenderCheckNotifies.some(
(frame) => frame.name === name && frame.claimed === true
),
backNames.claim,
{ timeout: 10_000, polling: 50 }
)
}
const between = (left) => left !== null && left > 0 && left < VIEWPORT.width
const sliding = (frames) => frames.filter((frame) => between(frame.session))
const SESSION_SETTLED = { list: null, session: 0, hit: 'session', from: null }
const LIST_SETTLED = { list: 0, session: null, hit: 'list', from: null }
const sessionMounts = (page) => page.evaluate(() => globalThis.__orcaSessionMounts ?? 0)
describeRender('the host stack transition on the page', () => {
for (const engine of ENGINES) {
describe(engine.name, () => {
const open = (options) => openList(browsers.get(engine.name), options)
it('slides the session in from the right on push, over the list', async () => {
const { errors, page } = await open()
const frames = await sampleFrames(page, 'push')
const firstShown = frames.find((frame) => frame.session !== null)
// Native-stack web fallback fails: first visible session frame is already x=0.
expect(firstShown?.session).toBeGreaterThan(0)
expect(frames.some((frame) => between(frame.session) && frame.list === 0)).toBe(true)
// Underlying list stays painted but cannot receive taps that would double-push.
expect(sliding(frames).map((frame) => frame.hit)).toEqual(sliding(frames).map(() => null))
expect(frames.at(-1)).toEqual(SESSION_SETTLED)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('slides the session out to the right on Back, revealing the list', async () => {
const { errors, page } = await open()
await sampleFrames(page, 'push')
expect(await sessionMounts(page)).toBe(1)
const frames = await sampleFrames(page, 'back')
// Native-stack web fallback removes popped screen on first frame instead of sliding out.
expect(frames.some((frame) => between(frame.session) && frame.list === 0)).toBe(true)
// Exit must retain original screen instance.
expect(await sessionMounts(page)).toBe(1)
expect(frames.at(-1)).toEqual(LIST_SETTLED)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('pops with the slide on the device Back key and drops the popped screen', async () => {
const { errors, page } = await open()
const baseline = await nodeCount(page)
await sampleFrames(page, 'push')
await waitForBackClaim(page)
const frames = await sampleFrames(page, 'hardwareBack')
expect(frames.some((frame) => between(frame.session) && frame.list === 0)).toBe(true)
expect(frames.at(-1)).toEqual(LIST_SETTLED)
expect(await nodeCount(page)).toBe(baseline)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('leaves no slot behind when a push interrupts a pop', async () => {
const { errors, page } = await open()
await sampleFrames(page, 'push')
const pushed = await nodeCount(page)
const frames = await sampleFrames(page, 'back', {
followUp: 'push',
afterFrames: 3,
observeFullWindow: true
})
expect(frames.slice(0, 3).some((frame) => between(frame.session))).toBe(true)
expect(frames.at(-1)).toEqual(SESSION_SETTLED)
expect(await nodeCount(page)).toBe(pushed)
expect(
await page.evaluate(
() => document.querySelectorAll('[data-testid="stack-probe-session"]').length
)
).toBe(1)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('settles on the list when Back interrupts an unfinished push', async () => {
const { errors, page } = await open()
// Warm session chunk so interrupted push measures mid-slide, not pending content.
await sampleFrames(page, 'push')
await sampleFrames(page, 'back')
const baseline = await nodeCount(page)
const mounts = await sessionMounts(page)
const frames = await sampleFrames(page, 'push', {
followUp: 'back',
afterFrames: 6,
observeFullWindow: true
})
expect(between(frames[5].session)).toBe(true)
// Exit must start where interrupted entry stopped, rather than restarting at zero.
const exitFrom = frames.slice(6).find((frame) => frame.from?.startsWith('matrix'))?.from
const startX = Number(exitFrom?.match(/matrix\(1, 0, 0, 1, ([\d.]+), 0\)/)?.[1])
expect(between(startX)).toBe(true)
const leaving = frames
.slice(6)
.map((frame) => frame.session)
.filter((left) => left !== null)
expect(leaving).toEqual(leaving.toSorted((a, b) => a - b))
expect(frames.at(-1)).toEqual(LIST_SETTLED)
expect(await nodeCount(page)).toBe(baseline)
expect(await sessionMounts(page)).toBe(mounts + 1)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('slides a screen out from rest after a push that once interrupted its entry', async () => {
const { errors, page } = await open()
await sampleFrames(page, 'push')
await sampleFrames(page, 'back')
// Covered wt-1 must finish its ongoing entry beneath wt-2.
await sampleFrames(page, 'push', { followUp: 'pushOther', afterFrames: 6 })
await sampleFrames(page, 'back')
const frames = await sampleFrames(page, 'back')
expect(frames.find((frame) => frame.from !== null)?.from).toMatch(/^translateX\(0(px)?\)$/)
expect(frames.at(-1)).toEqual(LIST_SETTLED)
expect(errors).toEqual([])
await page.close()
}, 120_000)
it('swaps instantly in the tablet split view and under reduced motion', async () => {
for (const options of [{ animation: 'none' }, { reducedMotion: 'reduce' }]) {
const { errors, page } = await open(options)
const pushed = await sampleFrames(page, 'push')
expect(pushed.filter((frame) => between(frame.session))).toEqual([])
expect(pushed.at(-1)).toEqual(SESSION_SETTLED)
const popped = await sampleFrames(page, 'back')
expect(popped.filter((frame) => between(frame.session))).toEqual([])
expect(popped.at(-1)).toEqual(LIST_SETTLED)
expect(errors).toEqual([])
await page.close()
}
}, 120_000)
})
}
})