Files
orca/mobile/src/mobile-web-shell/mobile-web-shell-session.test.ts
T
OrcaWinandOrcaWin d91a3d5804 test: strengthen CLI, lifecycle, bundle and build coverage (audit 8) (#32777)
* test: audit repository visibility and session-search CLI coverage

* test: verify service identities and public skill-sharing output

* test: verify terminal retry commands and unsuccessful navigation attempts

* test: exercise bulk-close refusal and printed legacy prompt receipts

* test: audit host discovery and agent launch recovery identities

* test: audit ACP replay, lifecycle, and hook delivery coverage

* test: audit automation errors and build artifact guards

* test: audit mobile bridge and bundle lifecycle coverage

* docs: reconcile audit tracker with input-ownership changes

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 11:06:37 -07:00

878 lines
35 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
createMobileWebShellSession,
readyAgain,
CACHED,
MANIFEST,
ROUTE,
afterCacheRead,
gates,
readySession,
run,
started,
BUNDLE_REFUSED,
LINK_LOST,
withoutRecord
} from './mobile-web-shell-session-test-fixtures'
import { shellPageFrame } from './shell-page-frame'
const NO_BUNDLE_WALL = { kind: 'wall', verdict: { kind: 'blocked', reason: 'bundle-unavailable' } }
describe('the gates decide whether a step is taken at all', () => {
it('waits while a connection is still being made', () => {
const step = started({ reachability: 'connecting' })
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.effects).toEqual([])
})
it('waits while status.get is still pending rather than reading its empty capabilities', () => {
const step = started({ statusPending: true, hostCapabilities: [] })
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.effects).toEqual([])
})
it('says a status could not be read rather than walling or waiting on it forever', () => {
const step = started({ statusReadable: false, hostCapabilities: [] })
expect(step.session.state).toEqual({
kind: 'failed',
reason: 'status-unreadable',
retriedOnce: false
})
expect(step.effects).toEqual([])
})
it('picks the flow back up if that status ever becomes readable', () => {
const unreadable = started({ statusReadable: false, hostCapabilities: [] })
const step = run(unreadable.session, { type: 'gates-changed', gates: gates() })
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.effects).toEqual([{ kind: 'open-cache' }])
})
it('walls a readable host that serves no bundle rather than falling back to a native screen', () => {
const step = started({ hostCapabilities: [] })
expect(step.session.state).toEqual(NO_BUNDLE_WALL)
expect(step.effects).toEqual([])
})
it('sweeps and reads the cache once the capability is answered', () => {
expect(started().effects).toEqual([{ kind: 'open-cache' }])
})
it('reads the cache for an unreachable host too, before deciding anything', () => {
expect(started({ reachability: 'unreachable' }).effects).toEqual([{ kind: 'open-cache' }])
})
})
describe('the offline rule', () => {
it("shows offline, not a wall, when an unreachable host's cached bundle lists no such route", () => {
const offline = started({ reachability: 'unreachable' })
const step = run(offline.session, {
type: 'cache-read',
generation: { ...CACHED, routes: [] }
})
expect(step.session.state).toEqual({ kind: 'offline' })
expect(step.effects).toEqual([])
})
it('opens a cached generation with no compat check when the host is unreachable', () => {
const start = started({ reachability: 'unreachable', hostCapabilities: [] })
const step = run(start.session, { type: 'cache-read', generation: CACHED })
expect(step.session.state).toEqual({ kind: 'activating', source: 'cache' })
expect(step.effects).toEqual([
{
kind: 'open-generation',
directory: CACHED.directory,
buildId: CACHED.buildId,
totalBytes: CACHED.totalBytes
}
])
})
it('says so when an unreachable host has nothing cached', () => {
const start = started({ reachability: 'unreachable' })
const step = run(start.session, { type: 'cache-read', generation: null })
expect(step.session.state).toEqual({ kind: 'offline' })
expect(step.effects).toEqual([])
})
it('waits on a cache read that lands mid-dial instead of opening it unchecked', () => {
const dialling = run(started().session, {
type: 'gates-changed',
gates: gates({ reachability: 'connecting' })
})
const step = run(dialling.session, { type: 'cache-read', generation: CACHED })
// Connecting is not unreachable: the compat check is a moment away, and skipping it would put a
// generation on screen the host is about to say it no longer serves.
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.session.cached).toEqual(CACHED)
expect(step.effects).toEqual([])
})
it('restarts the flow when the host becomes reachable while offline is showing', () => {
const offline = run(started({ reachability: 'unreachable' }).session, {
type: 'cache-read',
generation: null
})
const step = run(offline.session, { type: 'gates-changed', gates: gates() })
expect(step.effects).toEqual([{ kind: 'open-cache' }])
})
})
describe('the connected flow', () => {
it('asks the host for a manifest once the cache has been read', () => {
expect(afterCacheRead(null).effects).toEqual([{ kind: 'read-manifest' }])
expect(afterCacheRead(CACHED).effects).toEqual([{ kind: 'read-manifest' }])
})
it('walls a manifest written in a schema this shell does not know', () => {
const step = run(afterCacheRead(null).session, {
type: 'manifest-read',
manifest: { ...MANIFEST, schemaVersion: 99 }
})
expect(step.session.state).toEqual({
kind: 'wall',
verdict: { kind: 'blocked', reason: 'bundle-shell-too-old', schemaVersion: 99 }
})
expect(step.effects).toEqual([])
})
it('opens a same-build hit without paging, under the fresh grants', () => {
// Stored routes this shell cannot serve: only the fresh manifest's grants make it a page.
const stale = { ...CACHED, routes: [{ pathname: '/h/[hostId]', grants: ['teleport'] }] }
const step = run(afterCacheRead(stale).session, { type: 'manifest-read', manifest: MANIFEST })
expect(step.session.state).toEqual({ kind: 'activating', source: 'cache' })
// Only the open: a write beside the generation would race the native view reading it.
expect(step.effects).toEqual([
{
kind: 'open-generation',
directory: CACHED.directory,
buildId: CACHED.buildId,
totalBytes: CACHED.totalBytes
}
])
expect(step.session.routeGrants).toEqual(['navigate'])
})
it('downloads when the cached build id is a different one', () => {
const stale = { ...CACHED, buildId: 'c'.repeat(64) }
const step = run(afterCacheRead(stale).session, { type: 'manifest-read', manifest: MANIFEST })
expect(step.session.state).toEqual({
kind: 'fetching',
completedAssets: 0,
totalAssets: 4,
receivedBytes: 0,
totalBytes: 4096
})
expect(step.effects).toEqual([{ kind: 'download' }])
})
it('downloads when there is no cache at all', () => {
const step = run(afterCacheRead(null).session, { type: 'manifest-read', manifest: MANIFEST })
expect(step.effects).toEqual([{ kind: 'download' }])
})
it('walls a listed route whose page is older than this shell, as a desktop to update', () => {
for (const manifest of [
{ ...MANIFEST, pageVersion: 0 },
{ ...MANIFEST, pageVersion: undefined }
]) {
const step = run(afterCacheRead(null).session, { type: 'manifest-read', manifest })
expect(step.session.state).toEqual({
kind: 'wall',
verdict: {
kind: 'blocked',
reason: 'bundle-incompatible',
side: 'desktop',
pageVersion: 0,
requiredPageVersion: 1
}
})
}
})
it('tells the page which routes it may keep, so it hands the rest back', () => {
const step = run(afterCacheRead(null).session, { type: 'manifest-read', manifest: MANIFEST })
expect(step.session.pageRoutes).toEqual(['/h/[hostId]'])
})
it('carries download progress and then stages and activates', () => {
const fetching = run(afterCacheRead(null).session, {
type: 'manifest-read',
manifest: MANIFEST
})
const progressed = run(fetching.session, {
type: 'fetch-progress',
completedAssets: 2,
totalAssets: 4,
receivedBytes: 2048,
totalBytes: 4096
})
expect(progressed.session.state).toMatchObject({ kind: 'fetching', completedAssets: 2 })
const staged = run(progressed.session, { type: 'download-staged' })
expect(staged.session.state).toEqual({ kind: 'activating', source: 'download' })
const ready = run(staged.session, {
type: 'activated',
generationDirectory: '/cache/gen',
sessionId: 'session-one',
buildId: MANIFEST.buildId,
totalBytes: 4096,
elapsedMs: 900
})
expect(ready.session.state).toEqual({
kind: 'ready',
generationDirectory: '/cache/gen',
sessionId: 'session-one',
buildId: MANIFEST.buildId,
totalBytes: 4096,
elapsedMs: 900
})
})
it('ignores progress that arrives after the fetching state is gone', () => {
const ready = readySession()
const step = run(ready.session, {
type: 'fetch-progress',
completedAssets: 1,
totalAssets: 4,
receivedBytes: 1,
totalBytes: 4096
})
expect(step.session.state).toEqual(ready.session.state)
})
it('fails when the download or the cache write never produced a generation', () => {
const step = run(afterCacheRead(null).session, {
type: 'download-failed',
cause: BUNDLE_REFUSED
})
expect(step.session.state).toEqual({
kind: 'failed',
reason: 'download-failed',
retriedOnce: false
})
})
})
describe('a read the link cut short falls back to what is on disk', () => {
/** Connected, a generation cached, the manifest read in flight — where the drop is felt. */
function manifestInFlight() {
return afterCacheRead(CACHED)
}
it('opens the cached generation when the socket drops before the reachability change does', () => {
const step = run(manifestInFlight().session, { type: 'download-failed', cause: LINK_LOST })
expect(step.session.state).toEqual({ kind: 'activating', source: 'cache' })
expect(withoutRecord(step.effects)).toEqual([
{
kind: 'open-generation',
directory: CACHED.directory,
buildId: CACHED.buildId,
totalBytes: CACHED.totalBytes
}
])
const ready = run(step.session, {
type: 'activated',
generationDirectory: CACHED.directory,
sessionId: 'session-one',
buildId: CACHED.buildId,
totalBytes: CACHED.totalBytes,
elapsedMs: 12
})
expect(ready.session.state).toMatchObject({ kind: 'ready', buildId: CACHED.buildId })
})
it('still says the workspace could not be downloaded when nothing is on disk', () => {
const step = run(afterCacheRead(null).session, {
type: 'download-failed',
cause: LINK_LOST
})
expect(step.session.state).toEqual({
kind: 'failed',
reason: 'download-failed',
retriedOnce: false
})
expect(withoutRecord(step.effects)).toEqual([])
})
it('leaves no notice on it, because nothing says an update was there to fail', () => {
// The link went before the host said what it serves. "Update failed" would be a claim about a
// generation this phone never heard of.
const step = run(manifestInFlight().session, { type: 'download-failed', cause: LINK_LOST })
expect(step.session.updateNotice).toBeNull()
})
})
describe('a displayed generation is not restarted by the gates', () => {
it.each(['connected', 'unreachable', 'connecting'] as const)(
'keeps a ready session when reachability becomes %s',
(reachability) => {
const ready = readySession()
const step = run(ready.session, { type: 'gates-changed', gates: gates({ reachability }) })
expect(step.session.state).toEqual(ready.session.state)
expect(step.effects).toEqual([])
}
)
it('keeps a wall and a terminal failure', () => {
const wall = started({ hostCapabilities: [] })
expect(run(wall.session, { type: 'gates-changed', gates: gates() }).effects).toEqual([])
const failed = run(afterCacheRead(null).session, {
type: 'download-failed',
cause: BUNDLE_REFUSED
})
expect(run(failed.session, { type: 'gates-changed', gates: gates() }).effects).toEqual([])
})
})
describe('recovery follows the shell view contract', () => {
it.each(['generation-unreadable', 'document-load-failed'] as const)(
'deletes this host cache and runs once more on %s',
(reason) => {
const step = run(readySession().session, { type: 'shell-failed', reason })
expect(step.effects).toEqual([{ kind: 'delete-cache' }, { kind: 'open-cache' }])
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.session.retriedOnce).toBe(true)
expect(step.session.cached).toBeNull()
}
)
it('takes a recovery through the gate rather than back to a manifest check', () => {
const ready = readySession()
// A reconnect whose status probe failed. Stored, not acted on: a workspace on screen is not
// restarted by a gates change, which is how a ready session ends up holding one like this.
const stale = run(ready.session, {
type: 'gates-changed',
gates: gates({ statusReadable: false, hostCapabilities: [] })
})
expect(stale.session.state).toMatchObject({ kind: 'ready' })
const step = run(stale.session, { type: 'shell-failed', reason: 'document-load-failed' })
// Not the wall the empty capability list would have produced, which nothing leaves.
expect(step.session.state).toEqual({
kind: 'failed',
reason: 'status-unreadable',
retriedOnce: true
})
expect(step.effects).toEqual([{ kind: 'delete-cache' }])
const rearmed = run(step.session, { type: 'gates-changed', gates: gates() })
expect(rearmed.session.state).toEqual({ kind: 'checking' })
expect(rearmed.effects).toEqual([{ kind: 'open-cache' }])
})
it('walls a recovery when the host no longer serves a bundle', () => {
const stale = run(readySession().session, {
type: 'gates-changed',
gates: gates({ hostCapabilities: [] })
})
const step = run(stale.session, { type: 'shell-failed', reason: 'document-load-failed' })
expect(step.session.state).toEqual(NO_BUNDLE_WALL)
// The cache still goes: the bytes that failed are suspect whatever screen follows them.
expect(step.effects).toEqual([{ kind: 'delete-cache' }])
})
it('deletes the suspect cache and waits when the recovery lands mid-reconnect', () => {
const dialling = run(readySession().session, {
type: 'gates-changed',
gates: gates({ reachability: 'connecting' })
})
const step = run(dialling.session, { type: 'shell-failed', reason: 'generation-unreadable' })
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.effects).toEqual([{ kind: 'delete-cache' }])
expect(run(step.session, { type: 'gates-changed', gates: gates() }).effects).toEqual([
{ kind: 'open-cache' }
])
})
it.each(['generation-unreadable', 'document-load-failed'] as const)(
'is terminal the second time %s is reported',
(reason) => {
const first = run(readySession().session, { type: 'shell-failed', reason })
const refetched = readyAgain(first.session, 'session-two')
const second = run(refetched.session, { type: 'shell-failed', reason })
expect(second.effects).toEqual([])
expect(second.session.state).toEqual({ kind: 'failed', reason, retriedOnce: true })
}
)
it('remounts once on render-process-gone and never deletes anything', () => {
const ready = readySession()
const step = run(ready.session, { type: 'shell-failed', reason: 'render-process-gone' })
expect(step.effects).toEqual([{ kind: 'remount' }])
expect(step.session.state).toEqual(ready.session.state)
const remounted = run(step.session, { type: 'remounted', sessionId: 'session-two' })
expect(remounted.session.state).toMatchObject({
kind: 'ready',
sessionId: 'session-two',
generationDirectory: CACHED.directory
})
})
it('is terminal the second time the render process is gone, still without a delete', () => {
const first = run(readySession().session, {
type: 'shell-failed',
reason: 'render-process-gone'
})
const remounted = run(first.session, { type: 'remounted', sessionId: 'session-two' })
const second = run(remounted.session, { type: 'shell-failed', reason: 'render-process-gone' })
expect(second.effects).toEqual([])
expect(second.session.state).toEqual({
kind: 'failed',
reason: 'render-process-gone',
retriedOnce: false
})
})
it('is terminal on the first isolation-unavailable, with no retry and no delete', () => {
const step = run(readySession().session, {
type: 'shell-failed',
reason: 'isolation-unavailable'
})
expect(step.effects).toEqual([])
expect(step.session.state).toEqual({
kind: 'failed',
reason: 'isolation-unavailable',
retriedOnce: false
})
})
it('ignores a session id for a generation that is no longer ready', () => {
const step = run(started().session, { type: 'remounted', sessionId: 'session-two' })
expect(step.session.state).toEqual({ kind: 'checking' })
})
})
describe('try again', () => {
it('clears both latches and restarts the flow', () => {
const lost = run(readySession().session, {
type: 'shell-failed',
reason: 'render-process-gone'
})
const remounted = run(lost.session, { type: 'remounted', sessionId: 'session-remounted' })
const first = run(remounted.session, {
type: 'shell-failed',
reason: 'document-load-failed'
})
const refetched = readyAgain(first.session, 'session-two')
const failed = run(refetched.session, { type: 'shell-failed', reason: 'document-load-failed' })
expect(failed.session.retriedOnce).toBe(true)
expect(failed.session.remountedOnce).toBe(true)
const retried = run(failed.session, { type: 'retry-pressed' })
expect(retried.session.retriedOnce).toBe(false)
expect(retried.session.remountedOnce).toBe(false)
expect(retried.session.cached).toBeNull()
expect(retried.effects).toEqual([{ kind: 'open-cache' }])
// And the delete-and-refetch is available again.
const again = run(
run(retried.session, { type: 'cache-read', generation: CACHED }).session,
{ type: 'manifest-read', manifest: MANIFEST },
{
type: 'activated',
generationDirectory: CACHED.directory,
sessionId: 'session-three',
buildId: MANIFEST.buildId,
totalBytes: 4096,
elapsedMs: 3
},
{ type: 'shell-failed', reason: 'document-load-failed' }
)
expect(again.effects).toEqual([{ kind: 'delete-cache' }, { kind: 'open-cache' }])
})
it('stays walled rather than looping when the host still serves no bundle', () => {
const walled = started({ hostCapabilities: [] })
const retried = run(walled.session, { type: 'retry-pressed' })
expect(retried.session.state).toEqual(NO_BUNDLE_WALL)
expect(retried.effects).toEqual([])
})
it('does nothing but reset when no gates have arrived yet', () => {
const step = run(createMobileWebShellSession(ROUTE), { type: 'retry-pressed' })
expect(step.session.state).toEqual({ kind: 'checking' })
expect(step.effects).toEqual([])
})
})
describe('a result from a superseded flow reports into nothing', () => {
it('drops the cache read of a run a reconnect replaced, so nothing opens unchecked', () => {
const first = started({ reachability: 'unreachable' })
const restarted = run(first.session, { type: 'gates-changed', gates: gates() })
expect(restarted.effects).toEqual([{ kind: 'open-cache' }])
// The offline read would have opened this generation with no compat check at all.
const stale = run(restarted.session, {
type: 'cache-read',
flow: first.session.flow,
generation: CACHED
})
expect(stale.effects).toEqual([])
expect(stale.session.cached).toBeNull()
expect(run(stale.session, { type: 'cache-read', generation: CACHED }).effects).toEqual([
{ kind: 'read-manifest' }
])
})
it('drops the manifest of a run the socket drop replaced, so no download is asked for', () => {
const first = afterCacheRead(null)
const restarted = run(first.session, {
type: 'gates-changed',
gates: gates({ reachability: 'unreachable' })
})
const stale = run(restarted.session, {
type: 'manifest-read',
flow: first.session.flow,
manifest: MANIFEST
})
expect(stale.effects).toEqual([])
expect(stale.session.state).toEqual({ kind: 'checking' })
const current = run(stale.session, { type: 'cache-read', generation: null })
expect(current.session.state).toEqual({ kind: 'offline' })
expect(current.effects).toEqual([])
})
it('keeps a workspace on screen when the manifest read the drop abandoned finally rejects', () => {
// The reproduced sequence: connected, cache read, manifest in flight, socket drops, the offline
// path opens the cached generation, and only then does the abandoned RPC settle.
const inFlight = afterCacheRead(CACHED)
const offline = run(inFlight.session, {
type: 'gates-changed',
gates: gates({ reachability: 'unreachable' })
})
const ready = run(
offline.session,
{ type: 'cache-read', generation: CACHED },
{
type: 'activated',
generationDirectory: CACHED.directory,
sessionId: 'session-one',
buildId: CACHED.buildId,
totalBytes: CACHED.totalBytes,
elapsedMs: 4
}
)
expect(ready.session.state).toMatchObject({ kind: 'ready' })
const late = run(ready.session, {
type: 'download-failed',
cause: BUNDLE_REFUSED,
flow: inFlight.session.flow
})
expect(late.session.state).toEqual(ready.session.state)
})
it('applies a remount of the current flow and ignores one from a replaced run', () => {
const ready = readySession()
const remounting = run(ready.session, { type: 'shell-failed', reason: 'render-process-gone' })
const stale = run(remounting.session, {
type: 'remounted',
flow: remounting.session.flow - 1,
sessionId: 'session-stale'
})
expect(stale.session.state).toEqual(ready.session.state)
expect(
run(stale.session, { type: 'remounted', sessionId: 'session-two' }).session.state
).toMatchObject({ sessionId: 'session-two' })
})
})
describe('the remount budget is one per session, not one per reconnect', () => {
it('keeps the latch set when the gates restart the flow after a load failure', () => {
const remounted = run(
readySession().session,
{ type: 'shell-failed', reason: 'render-process-gone' },
{ type: 'remounted', sessionId: 'session-two' },
{ type: 'shell-failed', reason: 'document-load-failed' }
)
expect(remounted.session.remountedOnce).toBe(true)
const restarted = run(remounted.session, { type: 'gates-changed', gates: gates() })
expect(restarted.session.remountedOnce).toBe(true)
expect(run(restarted.session, { type: 'retry-pressed' }).session.remountedOnce).toBe(false)
})
})
describe('only the state that mounted the view hears the view', () => {
it('ignores the second failure of one native batch, leaving the first recovery running', () => {
const recovering = run(readySession().session, {
type: 'shell-failed',
reason: 'document-load-failed'
})
const batched = run(recovering.session, {
type: 'shell-failed',
reason: 'render-process-gone'
})
expect(batched.session.state).toEqual({ kind: 'checking' })
expect(batched.effects).toEqual([])
expect(batched.session.flow).toBe(recovering.session.flow)
// And the cache read the recovery already asked for still lands on the recovery.
expect(run(batched.session, { type: 'cache-read', generation: null }).effects).toEqual([
{ kind: 'read-manifest' }
])
})
it('leaves a wall standing when a view that is no longer mounted reports a failure', () => {
const wall = started({ hostCapabilities: [] })
const step = run(wall.session, { type: 'shell-failed', reason: 'isolation-unavailable' })
expect(step.session.state).toEqual(wall.session.state)
expect(step.effects).toEqual([])
})
})
describe('a gates change that says nothing new starts nothing', () => {
it('leaves a check in flight alone rather than sweeping and reading a second time', () => {
const checking = started()
const again = run(checking.session, { type: 'gates-changed', gates: gates() })
expect(again.effects).toEqual([])
expect(again.session.flow).toBe(checking.session.flow)
})
it('holds the offline screen through a reconnect cycle that never reaches the host', () => {
const offline = run(started({ reachability: 'unreachable' }).session, {
type: 'cache-read',
generation: null
})
const cycled = run(
offline.session,
{ type: 'gates-changed', gates: gates({ reachability: 'unreachable' }) },
{ type: 'gates-changed', gates: gates({ reachability: 'unreachable' }) }
)
expect(cycled.effects).toEqual([])
expect(cycled.session.state).toEqual({ kind: 'offline' })
})
it('restarts on the verdict that changed, not on the object that was rebuilt', () => {
const checking = started({ statusPending: true })
const settled = run(checking.session, { type: 'gates-changed', gates: gates() })
expect(settled.effects).toEqual([{ kind: 'open-cache' }])
})
it('walls a check in flight when the host stops serving a bundle', () => {
const checking = started()
const step = run(checking.session, {
type: 'gates-changed',
gates: gates({ hostCapabilities: [] })
})
expect(step.session.state).toEqual(NO_BUNDLE_WALL)
})
})
/**
* A document that commits and then says nothing.
*
* The WebView reports a finished load for a response it painted, which a bundle whose entry threw
* during evaluation still produces. Only the page's own first frame proves its code ran, so the
* wait between the two is where a blank screen would otherwise live forever.
*/
describe('the page has to speak for the document that loaded', () => {
it('arms one wait when the document finishes and the page has not spoken', () => {
const step = run(readySession().session, { type: 'document-loaded' })
expect(step.effects).toEqual([{ kind: 'await-page-ready' }])
expect(step.session.state.kind).toBe('ready')
})
it('arms nothing when the page spoke first, because there is nothing left to wait for', () => {
const step = run(readySession().session, { type: 'page-ready' }, { type: 'document-loaded' })
expect(step.effects).toEqual([])
expect(step.session.pageReady).toBe(true)
})
it('arms nothing outside ready, where no view exists to have loaded anything', () => {
const step = run(afterCacheRead(null).session, { type: 'document-loaded' })
expect(step.effects).toEqual([])
})
it('deletes the cache and runs the flow again when the wait expires', () => {
const ready = run(readySession().session, { type: 'document-loaded' })
const step = run(ready.session, { type: 'page-ready-deadline' })
// The same recovery `document-load-failed` gets from the view: the bytes on disk are suspect,
// so they go and the host is asked once more.
expect(step.effects).toEqual([{ kind: 'delete-cache' }, { kind: 'open-cache' }])
expect(step.session.retriedOnce).toBe(true)
expect(step.session.state.kind).toBe('checking')
})
it('does nothing when the wait expires after the page has spoken', () => {
const step = run(
readySession().session,
{ type: 'document-loaded' },
{ type: 'page-ready' },
{ type: 'page-ready-deadline' }
)
expect(step.effects).toEqual([])
expect(step.session.state.kind).toBe('ready')
})
it('drops the expiry of a wait a restarted flow left behind', () => {
const ready = run(readySession().session, { type: 'document-loaded' })
const step = run(
ready.session,
{ type: 'retry-pressed' },
{ type: 'page-ready-deadline', flow: ready.session.flow }
)
expect(step.session.state.kind).toBe('checking')
expect(step.session.retriedOnce).toBe(false)
})
it('makes the second document prove itself, rather than riding the first one word', () => {
const spoken = run(readySession().session, { type: 'page-ready' })
const remounted = run(spoken.session, { type: 'remounted', sessionId: 'session-two' })
expect(remounted.session.pageReady).toBe(false)
expect(run(remounted.session, { type: 'document-loaded' }).effects).toEqual([
{ kind: 'await-page-ready' }
])
})
it('leaves a remounted document its own wait when the first one expires late', () => {
const first = run(readySession().session, { type: 'document-loaded' }, { type: 'page-ready' })
const armed = first.session.flow
const second = run(
first.session,
{ type: 'shell-failed', reason: 'render-process-gone' },
{ type: 'remounted', sessionId: 'session-two' },
{ type: 'document-loaded' }
)
// The second document is inside its own wait and has not spoken yet, so the only thing that can
// keep the first document's expiry off it is the flow the remount started.
const step = run(second.session, { type: 'page-ready-deadline', flow: armed })
expect(step.effects).toEqual([])
expect(step.session.state).toMatchObject({ kind: 'ready', sessionId: 'session-two' })
})
it('makes a freshly activated generation prove itself too', () => {
const spoken = run(readySession().session, { type: 'page-ready' })
const reactivated = run(spoken.session, {
type: 'activated',
generationDirectory: CACHED.directory,
sessionId: 'session-three',
buildId: MANIFEST.buildId,
totalBytes: MANIFEST.totalBytes,
elapsedMs: 9
})
expect(reactivated.session.pageReady).toBe(false)
})
})
/** A load the view refuses or never commits reports nothing, so only a clock ends the wait (#28626). */
describe('the view has to finish the document it was given', () => {
it('arms the wait when a generation is put on screen', () => {
expect(readySession().effects).toContainEqual({ kind: 'await-document-load' })
})
it('arms it again for a new document and for a remount', () => {
const ready = readySession()
expect(run(ready.session, { type: 'document-started' }).effects).toEqual([
{ kind: 'await-document-load' }
])
expect(run(ready.session, { type: 'remounted', sessionId: 'session-two' }).effects).toEqual([
{ kind: 'await-document-load' }
])
})
it('deletes the cache and runs the flow again when no document ever finished', () => {
const started = run(readySession().session, { type: 'document-started' })
const step = run(started.session, { type: 'document-load-deadline' })
expect(step.effects).toEqual([{ kind: 'delete-cache' }, { kind: 'open-cache' }])
expect(step.session.state.kind).toBe('checking')
})
it('does nothing once the document finished or the page spoke', () => {
const loaded = run(readySession().session, { type: 'document-loaded' })
expect(run(loaded.session, { type: 'document-load-deadline' }).effects).toEqual([])
const spoke = run(readySession().session, { type: 'page-ready' })
expect(run(spoke.session, { type: 'document-load-deadline' }).session.state.kind).toBe('ready')
})
it('drops the wait a replaced document left behind', () => {
const ready = readySession()
const armed = ready.session.flow
const started = run(ready.session, { type: 'document-started' })
const step = run(started.session, { type: 'document-load-deadline', flow: armed })
expect(step.effects).toEqual([])
expect(step.session.state.kind).toBe('ready')
})
it('makes the next document finish on its own', () => {
const loaded = run(readySession().session, { type: 'document-loaded' })
const started = run(loaded.session, { type: 'document-started' })
expect(started.session.documentLoaded).toBe(false)
})
})
/**
* A commit is not a paint, and `ready` is posted before the page has built anything: the only
* thing that says the view is worth showing is the page saying so.
*/
describe('the page reporting a frame on screen', () => {
it('keeps the frame covered past ready until the report', () => {
const spoken = run(readySession().session, { type: 'page-ready' })
expect(spoken.session.pagePainted).toBe(false)
expect(shellPageFrame(spoken.session)).toBe('unpainted')
const painted = run(spoken.session, { type: 'page-painted' })
expect(painted.session.pagePainted).toBe(true)
expect(shellPageFrame(painted.session)).toBe('painted')
})
it('makes a remounted document report its own frame', () => {
const painted = run(readySession().session, { type: 'page-ready' }, { type: 'page-painted' })
const remounted = run(painted.session, { type: 'remounted', sessionId: 'session-two' })
expect(remounted.session.pagePainted).toBe(false)
expect(shellPageFrame(remounted.session)).toBe('unpainted')
})
it('makes a freshly activated generation report its own frame', () => {
const painted = run(readySession().session, { type: 'page-ready' }, { type: 'page-painted' })
const reactivated = run(painted.session, {
type: 'activated',
generationDirectory: CACHED.directory,
sessionId: 'session-three',
buildId: MANIFEST.buildId,
totalBytes: MANIFEST.totalBytes,
elapsedMs: 9
})
expect(reactivated.session.pagePainted).toBe(false)
})
it('makes the document that replaced a painted one inside this mount report its own frame', () => {
const painted = run(readySession().session, { type: 'page-ready' }, { type: 'page-painted' })
expect(shellPageFrame(painted.session)).toBe('painted')
// No new session and no new generation: the view reloaded under the one already on screen.
const restarted = run(painted.session, { type: 'document-started' })
expect(restarted.session.pagePainted).toBe(false)
const reasked = run(restarted.session, { type: 'page-ready' })
expect(shellPageFrame(reasked.session)).toBe('unpainted')
expect(shellPageFrame(run(reasked.session, { type: 'page-painted' }).session)).toBe('painted')
})
it('retires the wait the document it replaced armed', () => {
const loaded = run(readySession().session, { type: 'document-loaded' })
expect(loaded.effects).toEqual([{ kind: 'await-page-ready' }])
const armed = loaded.session.flow
const spoken = run(loaded.session, { type: 'page-ready' })
const restarted = run(spoken.session, { type: 'document-started' })
// The replacement is still loading and has said nothing, which is exactly what the retired
// document's deadline reads as a document that never loaded.
const expired = run(restarted.session, { type: 'page-ready-deadline', flow: armed })
expect(expired.session.state.kind).toBe('ready')
// And the replacement arms a wait of its own, so a document that really never speaks still
// takes the session down.
const reloaded = run(restarted.session, { type: 'document-loaded' })
expect(reloaded.effects).toEqual([{ kind: 'await-page-ready' }])
})
it('keeps the frame of a document that repeats its own handshake', () => {
const painted = run(
readySession().session,
{ type: 'page-ready' },
{ type: 'page-painted' },
{ type: 'page-ready' }
)
// The document never restarted, so its frame is still the one on screen.
expect(painted.session.pagePainted).toBe(true)
expect(shellPageFrame(painted.session)).toBe('painted')
})
it('records nothing from a page whose generation is no longer on screen', () => {
const failed = run(readySession().session, {
type: 'shell-failed',
reason: 'document-load-failed'
})
expect(failed.session.state.kind).not.toBe('ready')
expect(run(failed.session, { type: 'page-painted' }).session.pagePainted).toBe(false)
})
})