mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
* test: remove 60 low-value cases and orphaned conversion fixtures * docs(ci): list supported Windows SSH host cells * test: recognize direct Windows orcad server entry --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
272 lines
9.6 KiB
JavaScript
272 lines
9.6 KiB
JavaScript
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||
import { chromium } from 'playwright-core'
|
||
import { buildMobileWebAppBundle } from '../../build/mobile-web-app/build-mobile-web-app-bundle.mjs'
|
||
import { mobileWebAppDependenciesPresent } from '../mobile-web-app-bundle-dependencies.mjs'
|
||
import {
|
||
createBundleServer,
|
||
waitForPageFrames,
|
||
installShellDouble,
|
||
readBridgeFaultGrant,
|
||
readBridgePagePainted,
|
||
readBridgeProtocolVersion,
|
||
readShellCsp
|
||
} from '../preview/mobile-web-app-render-harness.mjs'
|
||
|
||
// Mount real ExpoRoot: only rendering proves synthesized RequireContext has expected shape.
|
||
const HOST_ROUTE = '/h/render-check-host'
|
||
|
||
const SHELL_SESSION_ID = 'render-check-session'
|
||
const SHELL_BUILD_ID = 'render-check-build'
|
||
// Install init host: web expo-secure-store is empty, otherwise valid host paints Host not found.
|
||
const SHELL_HOST = {
|
||
id: 'render-check-host',
|
||
name: 'Render Check Host',
|
||
endpoint: 'ws://render-check',
|
||
lastConnected: 1
|
||
}
|
||
|
||
// Root shards lack mobile dependencies; browser bundling runs in mobile_web_app, while CSP tests run in both.
|
||
const bundles = mobileWebAppDependenciesPresent()
|
||
const describeRender = bundles ? describe : describe.skip
|
||
|
||
let scratch
|
||
let server
|
||
let browser
|
||
let origin
|
||
let routeChunks = {}
|
||
let cspHeader = null
|
||
let bridgeVersion = null
|
||
let faultGrant = null
|
||
|
||
// Poison real route chunk to test boundary without substituting a synthetic route tree.
|
||
const poisonedChunks = new Set()
|
||
const POISON_MESSAGE = 'render check poisoned this route chunk'
|
||
|
||
// Hold chunks until released so pending-load assertions do not depend on winning a network race.
|
||
const heldChunks = new Map()
|
||
let paintName = null
|
||
|
||
beforeAll(async () => {
|
||
cspHeader = await readShellCsp()
|
||
bridgeVersion = await readBridgeProtocolVersion()
|
||
faultGrant = await readBridgeFaultGrant()
|
||
paintName = await readBridgePagePainted()
|
||
if (!bundles) {
|
||
return
|
||
}
|
||
scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-app-render-'))
|
||
const built = await buildMobileWebAppBundle({ outDir: join(scratch, 'bundle') })
|
||
const { outDir } = built
|
||
routeChunks = built.routeChunks
|
||
// Prepend throw to retain exports/linking; replacing body would test link failure rather than evaluation failure.
|
||
const served = await createBundleServer({
|
||
outDir,
|
||
cspHeader,
|
||
transformChunk: (path, real) =>
|
||
poisonedChunks.has(path)
|
||
? `throw new Error(${JSON.stringify(POISON_MESSAGE)});\n${real.toString('utf8')}`
|
||
: real,
|
||
handleRequest: (request, response, path) => {
|
||
const held = heldChunks.get(path)
|
||
if (!held) {
|
||
return false
|
||
}
|
||
held
|
||
.then(() => readFile(join(outDir, path.slice(1))))
|
||
.then((real) => {
|
||
response.writeHead(200, { 'content-type': 'text/javascript' })
|
||
response.end(real)
|
||
})
|
||
return true
|
||
}
|
||
})
|
||
server = served.server
|
||
origin = served.origin
|
||
// Use runner Chrome override to avoid CI browser downloads.
|
||
const executablePath = process.env.ORCA_MOBILE_WEB_RENDER_BROWSER
|
||
browser = await chromium.launch({ headless: true, ...(executablePath ? { executablePath } : {}) })
|
||
}, 180_000)
|
||
|
||
afterAll(async () => {
|
||
await browser?.close()
|
||
server?.close()
|
||
if (scratch) {
|
||
await rm(scratch, { recursive: true, force: true })
|
||
}
|
||
})
|
||
|
||
// Require route-specific content; Expo Unmatched also paints text without errors.
|
||
const UNMATCHED = 'Unmatched Route'
|
||
|
||
// Observe fetched scripts as proof navigation loads next chunk; undefined shellRoute omits double, null installs shell without screen.
|
||
async function openPage({
|
||
shellRoute,
|
||
shellHost = SHELL_HOST,
|
||
shellStorage = {},
|
||
shellGrants,
|
||
shellPageRoutes = null
|
||
} = {}) {
|
||
const page = await browser.newPage({ viewport: { width: 390, height: 844 } })
|
||
if (shellRoute !== undefined) {
|
||
// Install channel at document start like native shell; entry reads it before load.
|
||
await page.addInitScript(installShellDouble, {
|
||
version: bridgeVersion,
|
||
sessionId: SHELL_SESSION_ID,
|
||
buildId: SHELL_BUILD_ID,
|
||
route: shellRoute,
|
||
host: shellHost,
|
||
storage: shellStorage,
|
||
faultGrant,
|
||
grants: shellGrants ?? [faultGrant],
|
||
pageRoutes: shellPageRoutes
|
||
})
|
||
}
|
||
const errors = []
|
||
const scripts = []
|
||
let reportUncaught = () => {}
|
||
// Race mount wait with entry errors to report native-only import failures without 30s timeout.
|
||
// Resolve rather than reject: error can settle during goto before any waiter attaches.
|
||
const uncaught = new Promise((resolve) => {
|
||
reportUncaught = resolve
|
||
})
|
||
page.on('pageerror', (error) => {
|
||
errors.push(`${error.name}: ${error.message}`)
|
||
reportUncaught(error)
|
||
})
|
||
page.on('console', (message) => {
|
||
if (message.type() === 'error') {
|
||
errors.push(`console.error: ${message.text()}`)
|
||
}
|
||
})
|
||
page.on('response', (response) => {
|
||
const path = new URL(response.url()).pathname
|
||
if (response.status() === 200 && path.endsWith('.js')) {
|
||
scripts.push(path)
|
||
}
|
||
})
|
||
return { page, errors, scripts, uncaught }
|
||
}
|
||
|
||
// Wait for route-specific content after mounted: deferred route chunk can still leave suspense body empty.
|
||
async function waitForRoute({ page, errors, uncaught }, route, awaitText) {
|
||
const named = (cause, what) =>
|
||
new Error(`${route} ${what}: ${errors.join(' | ') || 'no page or console error'}`, { cause })
|
||
const race = async (wait) =>
|
||
Promise.race([
|
||
wait.then(
|
||
() => null,
|
||
(error) => error
|
||
),
|
||
uncaught
|
||
])
|
||
// Require committed Expo tree signal; root children can be an error/fallback. Timer polling works even when page never paints rAF.
|
||
const cause = await race(
|
||
page.waitForFunction(() => document.documentElement.dataset.orcaWebEntry === 'mounted', {
|
||
timeout: 30_000,
|
||
polling: 250
|
||
})
|
||
)
|
||
if (cause) {
|
||
const state = await page.evaluate(
|
||
() => document.documentElement.dataset.orcaWebEntry ?? 'absent'
|
||
)
|
||
throw named(cause, `never mounted (entry ${state})`)
|
||
}
|
||
const paintCause = await race(
|
||
page.waitForFunction((needle) => document.body.innerText.includes(needle), awaitText, {
|
||
timeout: 30_000,
|
||
polling: 250
|
||
})
|
||
)
|
||
if (paintCause) {
|
||
throw named(paintCause, `mounted but never painted ${JSON.stringify(awaitText)}`)
|
||
}
|
||
// Fold boundary faults into asserted errors: caught throws emit no pageerror and may leave blank body.
|
||
for (const fault of await page.evaluate(() => globalThis.__orcaRenderCheckFaults ?? [])) {
|
||
errors.push(`page fault: ${fault}`)
|
||
}
|
||
}
|
||
|
||
const paintReports = (page, name) =>
|
||
page.evaluate(
|
||
(paint) =>
|
||
(globalThis.__orcaRenderCheckNotifies ?? []).filter((frame) => frame.name === paint).length,
|
||
name
|
||
)
|
||
|
||
describeRender('the Route A page in a real browser', () => {
|
||
it('tells the shell when a route chunk throws, rather than sitting on a blank page', async () => {
|
||
const chunk = routeChunks['./h/[hostId]/index.tsx']
|
||
expect(chunk, Object.keys(routeChunks).join(' ')).toBeTruthy()
|
||
poisonedChunks.add(`/assets/${chunk}`)
|
||
try {
|
||
const opened = await openPage({ shellRoute: { pathname: HOST_ROUTE } })
|
||
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
|
||
const reported = await opened.page
|
||
.waitForFunction(
|
||
() => {
|
||
const faults = globalThis.__orcaRenderCheckFaults ?? []
|
||
return faults.length > 0 ? faults : null
|
||
},
|
||
{ timeout: 30_000, polling: 250 }
|
||
)
|
||
.then((handle) => handle.jsonValue())
|
||
// Require poisoned module’s exact error; any-fault check could pass with wrong boundary report.
|
||
expect(reported.join(' | ')).toContain(POISON_MESSAGE)
|
||
// Router shell commits mounted before deferred rejection; require boundary removes screen beneath it.
|
||
const text = await opened.page.evaluate(() => document.body.innerText)
|
||
expect(text).not.toContain('Host not found')
|
||
expect(text).not.toContain(UNMATCHED)
|
||
await opened.page.close()
|
||
} finally {
|
||
poisonedChunks.delete(`/assets/${chunk}`)
|
||
}
|
||
}, 60_000)
|
||
|
||
it('reports its frame from the route screen, not from the router shell above it', async () => {
|
||
// A paint reported before the route chunk arrives would uncover an empty body.
|
||
const chunk = routeChunks['./h/[hostId]/index.tsx']
|
||
expect(chunk, Object.keys(routeChunks).join(' ')).toBeTruthy()
|
||
const path = `/assets/${chunk}`
|
||
let arrive = () => {}
|
||
heldChunks.set(
|
||
path,
|
||
new Promise((resolve) => {
|
||
arrive = resolve
|
||
})
|
||
)
|
||
try {
|
||
const opened = await openPage({ shellRoute: { pathname: HOST_ROUTE } })
|
||
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
|
||
await opened.page.waitForFunction(
|
||
() => document.documentElement.dataset.orcaWebEntry === 'shell-ready',
|
||
{ timeout: 30_000, polling: 250 }
|
||
)
|
||
await waitForPageFrames(opened.page)
|
||
// The page waits for its route's held chunk before it mounts, so nothing may mount or report a paint meanwhile.
|
||
expect(await opened.page.evaluate(() => document.documentElement.dataset.orcaWebEntry)).toBe(
|
||
'shell-ready'
|
||
)
|
||
expect(await paintReports(opened.page, paintName)).toBe(0)
|
||
|
||
arrive()
|
||
await waitForRoute(opened, HOST_ROUTE, SHELL_HOST.name)
|
||
await opened.page.waitForFunction(
|
||
(name) =>
|
||
(globalThis.__orcaRenderCheckNotifies ?? []).filter((frame) => frame.name === name)
|
||
.length > 0,
|
||
paintName,
|
||
{ timeout: 30_000, polling: 250 }
|
||
)
|
||
expect(opened.errors).toEqual([])
|
||
await opened.page.close()
|
||
} finally {
|
||
arrive()
|
||
heldChunks.delete(path)
|
||
}
|
||
}, 120_000)
|
||
})
|