mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
The OTA download screen now shows a progress bar over bytes received, labelled "Downloading the latest app" with a "from <host> · X.X of Y.Y MB" caption, instead of a spinner with raw file and byte counts. Related offline and failure copy says "the latest app" instead of "the workspace". The bar barely moved before because React never committed progress during the download. SHA-256 of each asset ran in JS (3.8 s of a 5.7 s download), and React Native's scheduler runs queued native callbacks ahead of normal-priority renders, forcing a render only after 5 s. Assets are now hashed with expo-crypto's native digest (~5 ms total), so progress renders every 100–250 ms at 1 MB/s. Verified on an Android emulator and the iOS simulator. The bar glides between bursts of progress (moving average of burst gaps, clamped 200–2000 ms) and is ShellWaitingFrame's indicator, so the label keeps the same position as on "Opening workspace". On an uncapped local link, decryption still keeps the JS thread busy and the bar can jump; that is left for a follow-up.
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import { createElement } from 'react'
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { colors } from '../theme/mobile-theme'
|
|
|
|
const fades = vi.hoisted(() => ({ stops: 0, finished: true }))
|
|
|
|
vi.mock('react-native', () => ({
|
|
ActivityIndicator: 'ActivityIndicator',
|
|
Easing: { in: (fn: unknown) => fn, quad: 'quad' },
|
|
Animated: {
|
|
View: 'Animated.View',
|
|
Value: class {
|
|
setValue(): void {}
|
|
},
|
|
timing: () => ({
|
|
start: (done?: (result: { finished: boolean }) => void) => {
|
|
done?.({ finished: fades.finished })
|
|
},
|
|
stop: () => {
|
|
fades.stops += 1
|
|
}
|
|
})
|
|
},
|
|
StyleSheet: {
|
|
create: (styles: unknown) => styles,
|
|
absoluteFillObject: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }
|
|
},
|
|
Text: 'Text',
|
|
View: 'View'
|
|
}))
|
|
|
|
const { ShellPageCover } = await import('./ShellWaitingFrame')
|
|
|
|
function render(visible: boolean): ReactTestRenderer {
|
|
let tree: ReactTestRenderer | null = null
|
|
act(() => {
|
|
tree = create(createElement(ShellPageCover, { label: 'Opening workspace', visible }))
|
|
})
|
|
if (tree === null) {
|
|
throw new Error('the cover never rendered')
|
|
}
|
|
return tree
|
|
}
|
|
|
|
function cover(tree: ReactTestRenderer) {
|
|
return tree.root.findAllByProps({ testID: 'mobile-web-shell-cover' })[0] ?? null
|
|
}
|
|
|
|
/** The colour the cover fills with, read out of its style prop rather than assumed of its shape. */
|
|
function coverBackground(tree: ReactTestRenderer): unknown {
|
|
const style: unknown = cover(tree)?.props.style
|
|
const base: unknown = Array.isArray(style) ? style[0] : null
|
|
return typeof base === 'object' && base !== null && 'backgroundColor' in base
|
|
? base.backgroundColor
|
|
: null
|
|
}
|
|
|
|
describe('the frame the shell keeps over an unpainted page', () => {
|
|
it('is up while the page has not painted', () => {
|
|
const tree = render(true)
|
|
expect(cover(tree)).not.toBeNull()
|
|
})
|
|
|
|
it('paints the app surface and never black, so an empty view is never a hole', () => {
|
|
// The whole defect in one assertion: what shows while the WebView draws nothing is this.
|
|
const background = coverBackground(render(true))
|
|
expect(background).toBe(colors.bgBase)
|
|
expect(background).not.toBe('#000000')
|
|
})
|
|
|
|
it('never takes a touch, so a report that never lands leaves a usable page under it', () => {
|
|
expect(cover(render(true))?.props.pointerEvents).toBe('none')
|
|
})
|
|
|
|
it('goes once the page reports a frame', () => {
|
|
const tree = render(true)
|
|
act(() => {
|
|
tree.update(createElement(ShellPageCover, { label: 'Opening workspace', visible: false }))
|
|
})
|
|
expect(cover(tree)).toBeNull()
|
|
})
|
|
|
|
it('stays out of the way when a fade is cut short rather than staying opaque', () => {
|
|
// A platform that stops the fade reports `finished: false`; the cover keeps its element and
|
|
// its `pointerEvents: none`, which is a transparent inert layer rather than an opaque one.
|
|
fades.finished = false
|
|
const tree = render(true)
|
|
act(() => {
|
|
tree.update(createElement(ShellPageCover, { label: 'Opening workspace', visible: false }))
|
|
})
|
|
expect(cover(tree)?.props.pointerEvents).toBe('none')
|
|
fades.finished = true
|
|
})
|
|
|
|
it('stops a fade in flight when the view unmounts', () => {
|
|
const before = fades.stops
|
|
const tree = render(false)
|
|
act(() => {
|
|
tree.unmount()
|
|
})
|
|
expect(fades.stops).toBeGreaterThan(before)
|
|
})
|
|
})
|