mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
* [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>
368 lines
15 KiB
JavaScript
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)
|
|
})
|
|
}
|
|
})
|