Files
StarPilot/openpilot/starpilot/galaxy/tests/test_layout_preview.mjs
T
2026-10-26 20:38:11 -04:00

168 lines
9.3 KiB
JavaScript

import assert from "node:assert/strict"
import { LayoutPreviewFeed, PREVIEW_SCENES, sceneHidesWidget } from "../web/js/layout-preview.js"
const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve() }
const document = { version: 1, palette: { text: "#FFFFFFFF" }, layouts: { large: {}, compact: {} } }
const signLayout = { speed_limit: { x: 160, y: 80, enabled: true }, speed_limit_actions: { x: 16, y: 20, enabled: true } }
const signWidgets = { speed_limit: { width: 264, height: 82, visualInsetTop: 12 }, speed_limit_actions: { width: 440, height: 52 } }
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', signLayout, signWidgets), false, 'The chooser belongs to the sign and must not hide it')
assert.equal(sceneHidesWidget('engaged', 'compact', 'speed_limit', signLayout, signWidgets), false)
for (const scene of ['slc_pending', 'slc_max', 'slc_retained']) {
for (const profile of ['large', 'compact'])
assert.equal(sceneHidesWidget(scene, profile, 'speed_limit_actions'), scene === 'slc_pending')
assert.equal(sceneHidesWidget(scene, 'compact', 'speed_limit', signLayout, signWidgets), scene !== 'slc_pending')
}
assert.equal(sceneHidesWidget('slc_equal', 'compact', 'speed_limit_actions'), true)
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', signLayout,
{ ...signWidgets, speed_limit: { ...signWidgets.speed_limit, visualInsetTop: 0 } }), false)
const restoredLayout = { speed_limit: {x:330, y:20, enabled:true}, speed_limit_actions: {x:128, y:186, enabled:true} }
const restoredWidgets = { speed_limit: {width:118, height:132}, speed_limit_actions: {width:348, height:54} }
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', restoredLayout, restoredWidgets), false)
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', {
...restoredLayout, speed_limit: {...restoredLayout.speed_limit, y:60}
}, restoredWidgets), false)
function fixture() {
const requests = [], updates = [], timers = new Map(), revoked = []
let next = 0, created = 0, unauthorized = 0
const feed = new LayoutPreviewFeed({ publish: (value) => updates.push(value), unauthorized: () => unauthorized++,
fetcher: (url, options) => new Promise((resolve, reject) => requests.push({ url, options, resolve, reject })),
later: (fn) => { timers.set(++next, fn); return next }, cancelTimer: (id) => timers.delete(id),
urls: { createObjectURL: () => `blob:${++created}`, revokeObjectURL: (url) => revoked.push(url) } })
const tick = () => { const [id, fn] = [...timers.entries()].at(-1); timers.delete(id); fn() }
const reply = async (index, status = 200, type = "image/png") => {
requests[index].resolve({ ok: status === 200, status, headers: { get: () => type },
blob: async () => ({ size: 1024 }) }); await flush()
}
return { feed, requests, updates, timers, revoked, tick, reply, get unauthorized() { return unauthorized } }
}
assert.deepEqual(PREVIEW_SCENES.map(({ id }) => id), ["general", "engaged", "aol", "long_only", "experimental", "braking", "stopped",
"cem_stop_light", "cem_lead", "cem_curve", "slc_notice", "slc_pending", "slc_max", "slc_equal",
"slc_pedal", "slc_retained", "slc_disengaged", "slc_unavailable", "slc_display"])
assert.equal(PREVIEW_SCENES.find(({ id }) => id === "experimental").label, "Manual experimental")
for (const scene of ["general", "cem_stop_light", "cem_lead", "cem_curve", "slc_notice", "slc_pending", "slc_max", "slc_equal",
"slc_pedal", "slc_retained", "slc_disengaged", "slc_unavailable", "slc_display"]) {
const cem = fixture()
cem.feed.start(); cem.feed.update(document, "compact", scene); cem.tick()
assert.equal(JSON.parse(cem.requests[0].options.body).scene, scene)
await cem.reply(0)
assert.equal(cem.updates.at(-1).status, "ready")
cem.feed.stop()
}
const normal = fixture()
normal.feed.start()
normal.feed.update(document, "large", "engaged")
normal.feed.update(document, "compact", "aol")
assert.equal(normal.timers.size, 1)
normal.tick()
assert.equal(normal.requests.length, 1)
assert.equal(normal.requests[0].url, "./api/ui/layout/preview")
assert.deepEqual(JSON.parse(normal.requests[0].options.body), { document, profile: "compact", scene: "aol" })
assert.equal(normal.requests[0].options.credentials, "same-origin")
await normal.reply(0)
assert.equal(normal.updates.at(-1).url, "blob:1")
normal.feed.update(document, "compact", "aol")
assert.equal(normal.timers.size, 0, "identical synchronization does not rerender the preview")
normal.feed.update(document, "large", "braking")
assert.deepEqual(normal.revoked, [])
assert.equal(normal.updates.at(-1).status, "updating")
normal.tick()
const stale = normal.requests[1]
normal.feed.update(document, "large", "experimental")
assert.equal(stale.options.signal.aborted, true)
normal.tick()
assert.equal(normal.requests.length, 2) // The next request waits for the old one to settle.
await normal.reply(1)
assert.equal(normal.requests.length, 3)
assert.equal(normal.updates.at(-1).status, "updating")
await normal.reply(2)
assert.equal(normal.updates.at(-1).url, "blob:2")
normal.feed.update(document, "large", "experimental", true)
assert.deepEqual(normal.revoked, ["blob:1"])
assert.equal(normal.timers.size, 0)
assert.equal(normal.feed.currentUrl, "blob:2", "drag keeps the preview mounted")
assert.equal(normal.updates.at(-1).status, "editing")
normal.feed.update(document, "large", "experimental")
normal.tick(); await normal.reply(3)
normal.feed.stop()
assert.deepEqual(normal.revoked, ["blob:1", "blob:2", "blob:3"])
assert.equal(normal.updates.at(-1).url, null)
const failure = fixture()
failure.feed.start(); failure.feed.update(document, "large", "engaged"); failure.tick(); await failure.reply(0, 503)
assert.equal(failure.updates.at(-1).status, "unavailable")
assert.equal(failure.timers.size, 1) // Recover automatically without hiding the error.
failure.feed.retry(); failure.tick(); await failure.reply(1)
assert.equal(failure.updates.at(-1).status, "ready")
failure.feed.update(document, "large", "braking"); failure.tick(); await failure.reply(2, 200, "text/html")
assert.equal(failure.updates.at(-1).status, "unavailable")
assert.deepEqual(failure.revoked, [])
for (const [status, message] of [[403, /Turn off the vehicle to preview/], [429, /busy/]]) {
failure.feed.update(document, "large", "engaged"); failure.tick()
await failure.reply(failure.requests.length - 1, status)
assert.match(failure.updates.at(-1).error, message)
}
const auth = fixture()
auth.feed.start(); auth.feed.update(document, "large", "engaged"); auth.tick(); await auth.reply(0, 401)
assert.equal(auth.unauthorized, 1)
assert.equal(auth.feed.active, false)
const timeout = fixture()
timeout.feed.start(); timeout.feed.update(document, "large", "engaged"); timeout.tick()
timeout.tick()
assert.equal(timeout.requests[0].options.signal.aborted, true)
timeout.requests[0].reject(new Error("aborted")); await flush()
assert.equal(timeout.updates.at(-1).status, "unavailable")
// The backdrop asks for the real screen without widgets, and only its key decides when to refetch.
const backdrop = fixture()
backdrop.feed.backdrop = true
backdrop.feed.start()
backdrop.feed.update(document, "large", "aol", false, { palette: document.palette })
backdrop.tick()
assert.equal(JSON.parse(backdrop.requests[0].options.body).art, "backdrop")
assert.deepEqual(JSON.parse(backdrop.requests[0].options.body).document, document)
await backdrop.reply(0)
const moved = { ...document, layouts: { large: { current_speed: { x: 9, y: 9, enabled: true } }, compact: {} } }
backdrop.feed.update(moved, "large", "aol", false, { palette: document.palette })
assert.equal(backdrop.timers.size, 0)
backdrop.feed.update(moved, "large", "braking", false, { palette: document.palette })
assert.equal(backdrop.timers.size, 1)
backdrop.feed.stop()
const late = fixture()
late.feed.start(); late.feed.update(document, "large", "engaged"); late.tick(); late.feed.stop(); await late.reply(0)
assert.equal(late.updates.some((update) => update.url?.startsWith("blob:")), false)
console.log("Native layout preview: debounce, single flight, stale responses, drag fallback, cleanup, retry and auth passed")
const projection = fixture()
projection.feed.start()
const aaDocument = {version: 1, canvas: {width: 1920, height: 1080}, widgets: {}}
projection.feed.update(aaDocument, "projection", "engaged")
projection.tick()
assert.deepEqual(JSON.parse(projection.requests[0].options.body), {document: aaDocument, profile: "projection", scene: "engaged"})
await projection.reply(0)
assert.equal(projection.updates.at(-1).status, "ready")
aaDocument.widgets.current_speed = {x: 800, y: 0, enabled: true}
projection.feed.update(aaDocument, "projection", "engaged")
projection.tick()
await projection.reply(1)
assert.equal(projection.updates.at(-1).status, "ready")
assert.deepEqual(projection.revoked.length, 1)
const hung = fixture()
hung.feed.start(); hung.feed.update(document, "large", "engaged"); hung.tick()
hung.requests[0].resolve({ ok: true, status: 200, headers: { get: () => "image/png" }, blob: () => new Promise(() => {}) })
await flush()
hung.tick(); await flush()
assert.equal(hung.updates.at(-1).status, "unavailable", "body reads have a deadline even when abort is ignored")
hung.tick(); hung.tick()
assert.equal(hung.requests.length, 2, "a timed-out preview recovers automatically")
await hung.reply(1)
assert.equal(hung.updates.at(-1).status, "ready")
hung.feed.stop()