Files
StarPilot/openpilot/starpilot/galaxy/tests/test_onroad_layout.mjs
T

1223 lines
71 KiB
JavaScript

import assert from "node:assert/strict"
import * as documentHelpers from "../web/js/onroad-layout-document.js"
import * as layoutModule from "../web/js/onroad-layout.js"
import { ONROAD_LAYOUT_TEMPLATE } from "../web/js/onroad-layout-template.js"
import { readFileSync } from "node:fs"
import { LayoutWidgetPreview, OnroadLayoutFeed, OnroadLayoutPage, validSnapshot, validDocument, clampPlacement, previewPoint, withAlpha, widgetPalette,
withDisplay, layoutDocument, previewDocument } from "../web/js/onroad-layout.js"
import { SettingsPage, SETTINGS_SECTIONS } from "../web/js/settings.js"
import { loadCatalog } from "../web/js/startup.js"
const copy = (data) => JSON.parse(JSON.stringify(data))
const widget = (label, kind, width, height, x, y) => ({ label, kind, width, height, default: { x, y, enabled: true } })
export function snapshot() {
const metadata = { profiles: {
large: { label: "Large UI", width: 2160, height: 1080, bounds: { x: 30, y: 30, width: 1800, height: 1020 }, reservedZones: [], widgets: {
current_speed: widget("Current speed", "current_speed", 580, 300, 640, 30),
cruise_limits: widget("Cruise and speed limit", "cruise_limits", 480, 522, 88, 75),
speed_limit_actions: widget("Speed limit actions", "speed_limit_actions", 480, 108, 88, 567),
steering_wheel: widget("Steering wheel", "steering_wheel", 192, 192, 1588, 75),
driver_monitor: widget("Driver monitoring", "driver_monitor", 192, 192, 88, 808),
torque_bar: { ...widget("Torque bar", "torque_bar", 998, 240, 439, 759), layer: "underlay" },
} },
compact: { label: "Small UI", width: 536, height: 240, bounds: { x: 0, y: 0, width: 476, height: 240 },
protectedWidget: "speed_limit_actions", reservedZones: [], widgets: {
max_speed: widget("Maximum speed", "max_speed", 162, 162, 0, 0),
speed_limit: widget("Speed limit", "speed_limit", 118, 132, 330, 20),
speed_limit_actions: widget("Speed limit actions", "speed_limit_actions", 282, 54, 174, 180),
steering_wheel: widget("Steering wheel", "steering_wheel", 50, 50, 21, 176),
driver_monitor: widget("Driver monitoring", "driver_monitor", 60, 60, 16, 10),
torque_bar: { ...widget("Torque bar", "torque_bar", 260, 61, 116, 167), layer: "underlay" },
model_confidence: { ...widget("Model confidence", "model_confidence", 60, 80, 476, 0), bounds: { x: 0, y: 0, width: 536, height: 240 } },
conditional_mode: { ...widget("CEM and driving mode", "conditional_mode", 60, 80, 476, 80), bounds: { x: 0, y: 0, width: 536, height: 240 } },
following_distance: { ...widget("Following distance", "following_distance", 60, 80, 476, 160), bounds: { x: 0, y: 0, width: 536, height: 240 } },
} },
}, paletteFields: [
{ id: "cardFill", label: "Card fill", default: "#000000A6" },
{ id: "cardBorder", label: "Card border", default: "#C4CDD0B4" },
{ id: "text", label: "Text", default: "#FFFFFFFF" },
] }
const frame = { cardFill: "#00000000", cardBorder: "#00000000" }
const actions = { cardFill: "#0C1820EB", cardBorder: "#A6DFBEFF", text: "#FFFFFFFF" }
const colors = {
large: { current_speed: { text: null }, cruise_limits: { cardFill: null, cardBorder: null, text: null },
speed_limit_actions: actions, steering_wheel: { cardFill: null, cardBorder: "#00000000" }, driver_monitor: frame },
compact: { max_speed: { text: null }, speed_limit_actions: actions, driver_monitor: frame, model_confidence: frame,
conditional_mode: frame, following_distance: { ...frame, text: "#FFFFFFFF" } },
}
for (const [profile, data] of Object.entries(metadata.profiles))
for (const [id, widget] of Object.entries(data.widgets)) widget.colors = { ...colors[profile][id] }
metadata.profiles.large.widgets.steering_wheel.resizable = { min: 144, default: 192, max: 240 }
// As the server sends them: anchors for the sidebar-hidden road, and positions that may use its extra width.
const shifts = { steering_wheel: 1, current_speed: .5, torque_bar: .5 }
for (const [id, shift] of Object.entries(shifts)) metadata.profiles.large.widgets[id].anchorShift = shift
for (const id of Object.keys(metadata.profiles.large.widgets))
Object.assign(metadata.profiles.large.widgets[id], { expandable: true, bounds: { x: 30 - 300 * (shifts[id] || 0), y: 30, width: 2100, height: 1020 } })
metadata.profiles.compact.widgets.steering_wheel.resizable = { min: 40, default: 50, max: 70 }
metadata.roadColorFields = [{ id: 'path', label: 'Path', default: '#30FF9CFF' },
{ id: 'pathEdge', label: 'Path edges', default: '#00FF40FF' }, { id: 'laneLines', label: 'Lane lines', default: '#FFFFFFFF' }]
metadata.profiles.compact.widgets.speed_limit_actions.visualInsetTop = 32
const document = { version: 11, roadColors: { large: {}, compact: {} }, widgetColors: { large: {}, compact: {} }, palette: Object.fromEntries(metadata.paletteFields.map((field) => [field.id, field.default])),
layouts: Object.fromEntries(Object.entries(metadata.profiles).map(([id, profile]) => [id,
Object.fromEntries(Object.entries(profile.widgets).map(([key, definition]) => [key,
{ ...definition.default, ...(definition.resizable ? { size: definition.resizable.default } : {}) }]))])) }
return { document, defaults: copy(document), metadata, revision: "saved-revision", editable: true, valid: true, activeProfile: null }
}
// The editor keeps its public API while sharing the pure document helpers.
for (const name of ["layoutDocument", "previewDocument", "withDisplay", "orderedWidgetIds", "widgetPalette", "withAlpha", "widgetSize", "placementLimits", "clampBox", "clampPlacement", "overlapsReserved", "previewPoint", "validDocument", "validSnapshot"])
assert.equal(layoutModule[name], documentHelpers[name], name)
const data = snapshot()
const restoredCompact = copy(data)
for (const [id, width, height, x, y] of [["max_speed", 162, 162, 0, 0], ["speed_limit", 118, 132, 330, 20], ["speed_limit_actions", 348, 54, 128, 186]]) {
Object.assign(restoredCompact.metadata.profiles.compact.widgets[id], { width, height, default: {x, y, enabled:true} })
restoredCompact.document.layouts.compact[id] = {x, y, enabled:true}
}
delete restoredCompact.metadata.profiles.compact.widgets.speed_limit_actions.visualInsetTop
restoredCompact.defaults = copy(restoredCompact.document)
assert.equal(validSnapshot(restoredCompact), true, "Restored small-UI defaults are editable")
assert.equal(clampPlacement(restoredCompact.metadata.profiles.compact, "max_speed", 174, 25, restoredCompact.document.layouts.compact), null)
assert.deepEqual(clampPlacement(restoredCompact.metadata.profiles.compact, "speed_limit", 330, 20, restoredCompact.document.layouts.compact), {x:330, y:20})
assert.deepEqual(restoredCompact.document.layouts.large, data.document.layouts.large)
const withMode = copy(data)
const modeId = "driving_mode_descriptions"
withMode.metadata.profiles.compact.widgets[modeId] = {
...widget("Driving Mode Descriptions", modeId, 168, 60, 162, 22), colors: { text: "#FFFFFFFF" },
}
withMode.document.layouts.compact[modeId] = { x: 162, y: 22, enabled: false }
withMode.document.layouts.compact.speed_limit_actions.y = 45
withMode.defaults = copy(withMode.document)
assert.equal(validSnapshot(withMode), true)
assert.equal(clampPlacement(withMode.metadata.profiles.compact, modeId, 162, 22, withMode.document.layouts.compact), null)
withMode.document.layouts.compact[modeId].enabled = true
assert.equal(validDocument(withMode.document, withMode.metadata), false)
withMode.document.layouts.compact[modeId].y = 110
assert.equal(validDocument(withMode.document, withMode.metadata), true)
// Hidden widgets free their space, but adding them back still requires a valid placement.
{
const sample = copy(data), layout = sample.document.layouts.compact, profile = sample.metadata.profiles.compact
layout.max_speed.enabled = false
assert.deepEqual(clampPlacement(profile, "speed_limit_actions", 80, 80, layout), { x: 80, y: 80 })
Object.assign(layout.speed_limit_actions, { x: 80, y: 80 })
assert.equal(validSnapshot(sample), true)
assert.equal(clampPlacement(profile, "max_speed", 0, 0, layout), null)
layout.max_speed.enabled = true
assert.equal(validDocument(sample.document, sample.metadata), false)
assert.equal(clampPlacement(profile, "speed_limit_actions", 80, 80, layout), null)
layout.max_speed.enabled = false
layout.max_speed.x = -1
assert.equal(validDocument(sample.document, sample.metadata), false)
layout.max_speed.x = 0
layout.max_speed.enabled = "false"
assert.equal(validDocument(sample.document, sample.metadata), false)
}
assert.equal(withAlpha("#AABBCC80", 200 / 255), "#AABBCC64")
assert.equal(withAlpha("#AABBCCFF", .9), "#AABBCCe5")
assert.equal(validSnapshot(data), true)
assert.deepEqual(clampPlacement(data.metadata.profiles.compact, "torque_bar", 174, 179), { x: 174, y: 179 })
const registeredUnderlay = copy(data.metadata.profiles.compact)
registeredUnderlay.widgets.max_speed.layer = "underlay"
assert.deepEqual(clampPlacement(registeredUnderlay, "max_speed", 174, 19), { x: 174, y: 19 })
const alternateUnderlayKind = copy(data.metadata.profiles.compact)
alternateUnderlayKind.widgets.torque_bar.kind = "max_speed"
assert.deepEqual(clampPlacement(alternateUnderlayKind, "torque_bar", 174, 179), { x: 174, y: 179 })
for (const mutate of [
(value) => { value.document.layouts.large.fake = { x: 0, y: 0, enabled: true } },
(value) => { value.document.layouts.compact.max_speed.y = 79 },
(value) => { value.document.palette.text = "red" },
(value) => { value.document.layouts.large.current_speed.x = Infinity },
(value) => { value.document.layouts.compact.steering_wheel.size = 71 },
(value) => { value.document.layouts.compact.speed_limit_actions.size = 70 },
(value) => { delete value.document.layouts.compact },
(value) => { value.metadata.profiles.compact.bounds.width = 9999 },
(value) => { value.metadata.paletteFields[1].id = "text" },
(value) => { value.editable = "true" },
]) {
const invalid = copy(data); mutate(invalid); assert.equal(validSnapshot(invalid), false)
}
assert.equal(validDocument({ ...data.document, arbitrary: 1 }, data.metadata), false)
assert.equal(clampPlacement(data.metadata.profiles.compact, "max_speed", 999, 999), null)
assert.deepEqual(clampPlacement(data.metadata.profiles.compact, "max_speed", 999, 18), { x: 314, y: 18 })
assert.equal(clampPlacement(data.metadata.profiles.compact, "max_speed", 999, 19), null)
const movedActions = copy(data.document.layouts.compact)
movedActions.speed_limit_actions.x = 100
assert.deepEqual(clampPlacement(data.metadata.profiles.compact, "speed_limit_actions", 100, 180, data.document.layouts.compact), { x: 100, y: 180 })
assert.equal(clampPlacement(data.metadata.profiles.compact, "speed_limit_actions", 10, 180, data.document.layouts.compact), null)
assert.equal(clampPlacement(data.metadata.profiles.compact, "max_speed", 100, 19, movedActions), null)
assert.deepEqual(clampPlacement(data.metadata.profiles.compact, "speed_limit", 330, 20, movedActions), { x: 330, y: 20 })
assert.equal(validDocument({ ...data.document, layouts: { ...data.document.layouts, compact: movedActions } }, data.metadata), true)
const resizedWheel = copy(data.document)
resizedWheel.layouts.compact.steering_wheel.size = 70
resizedWheel.layouts.compact.steering_wheel.y = 160
assert.equal(validDocument(resizedWheel, data.metadata), true)
assert.equal(clampPlacement(data.metadata.profiles.compact, "steering_wheel", 150, 170, resizedWheel.layouts.compact), null)
assert.deepEqual(clampPlacement(data.metadata.profiles.compact, "steering_wheel", 21, 176, resizedWheel.layouts.compact), { x: 21, y: 170 })
assert.deepEqual(clampPlacement(data.metadata.profiles.large, "current_speed", -999, -99), { x: -120, y: 30 }, "the sidebar-hidden screen is reachable")
assert.deepEqual(clampPlacement(data.metadata.profiles.large, "cruise_limits", 9999, 30), { x: 2130 - 480, y: 30 })
assert.deepEqual(clampPlacement(data.metadata.profiles.large, "torque_bar", -999, 759), { x: -120, y: 759 }, "the torque bar reaches the sidebar-hidden screen")
assert.equal(clampPlacement(data.metadata.profiles.large, "fake", 30, 30), null)
assert.equal(clampPlacement(data.metadata.profiles.large, "current_speed", NaN, 30), null)
assert.deepEqual(previewPoint({ clientX: 316, clientY: 200 }, { left: 100, top: 92, width: 432, height: 216 }, data.metadata.profiles.large), { x: 1080, y: 540 })
function editor() {
const emitted = [], calls = [], backdropCalls = [], captures = new Set()
const target = { setPointerCapture(id) { captures.add(id) }, hasPointerCapture(id) { return captures.has(id) }, releasePointerCapture(id) { captures.delete(id) } }
const vm = { ...OnroadLayoutPage.setup({ mode: "local", unauthorized() {} }), $emit: (...args) => emitted.push(args),
$refs: { preview: { ...target, getBoundingClientRect: () => ({ left: 100, top: 100, width: 1080, height: 540 }) } } }
for (const [key, getter] of Object.entries(OnroadLayoutPage.computed)) Object.defineProperty(vm, key, { get: () => getter.call(vm) })
for (const [key, method] of Object.entries(OnroadLayoutPage.methods)) vm[key] = method.bind(vm)
// This editor fixture exercises interaction; native request/race behavior has its own actual feed tests.
vm.artFeed.start = () => { vm.artFeed.active = true }
vm.artFeed.update = () => {}
vm.artFeed.stop = () => { vm.artFeed.active = false }
vm.artFeed.pause = () => { vm.artFeed.active = false }
vm.backdropFeed.start = () => { vm.backdropFeed.active = true; backdropCalls.push("start") }
vm.backdropFeed.update = (...args) => backdropCalls.push(["update", ...args])
vm.backdropFeed.stop = () => { vm.backdropFeed.active = false; backdropCalls.push("stop") }
vm.state.data = snapshot(); vm.state.data.supportedProfiles = ['large', 'compact']
// These geometry fixtures represent a previously saved On choice.
vm.state.data.document.display = vm.state.data.defaults.display = { DeveloperSidebar: "On" }
vm.state.draft = copy(vm.state.data.document); vm.state.status = "ready"; vm.state.selected = "current_speed"
const publish = vm.feed.publish
vm.feed = { load: () => calls.push("load"), save: (document) => calls.push(copy(document)) }
const event = (x, y, pointerId = 1) => ({ clientX: x, clientY: y, pointerId, button: 0, currentTarget: { setPointerCapture() { throw new Error("Capture must belong to the stable preview") } }, preventDefault() {} })
return { vm, event, emitted, calls, backdropCalls, captures, publish }
}
// Snapping changes pointer placement only; a drag remains one history entry.
const snappedDrag = editor(), snapVm = snappedDrag.vm
const largePoint = (x, y, options = {}) => ({ ...snappedDrag.event(100 + x / 2, 100 + y / 2), pointerType: "mouse", ...options })
snapVm.startDrag("current_speed", largePoint(640, 30))
snapVm.moveDrag(largePoint(34, 430))
assert.equal(snapVm.layout.current_speed.x, 30)
assert.ok(snapVm.state.snapGuides.some(guide => guide.axis === "x" && guide.kind === "bounds"))
assert.equal(snapVm.snapSummary, "At the edge")
const cachedTargets = snapVm.state.drag.targets
snapVm.moveDrag(largePoint(35, 430))
assert.equal(snapVm.state.drag.targets, cachedTargets)
assert.equal(snapVm.snapInBar, false, "a mouse drag keeps the alignment chip on the preview")
snapVm.endDrag(largePoint(34, 430))
snapVm.startDrag("current_speed", largePoint(34, 430, { pointerType: "touch" }))
snapVm.moveDrag(largePoint(34, 300, { pointerType: "touch" })) // past the finger slop
snapVm.moveDrag(largePoint(35, 430, { pointerType: "touch" }))
assert.equal(snapVm.snapInBar, true, "a finger drag shows the alignment in the save bar instead")
snapVm.endDrag(largePoint(35, 430, { pointerType: "touch" }))
assert.equal(snapVm.snapInBar, false)
assert.deepEqual(snapVm.state.snapGuides, [])
assert.equal(snapVm.state.history.undo.length, 1)
snapVm.undo()
assert.deepEqual(snapVm.layout.current_speed, snapshot().document.layouts.large.current_speed)
for (const bypass of ["alt", "disabled"]) {
const free = editor().vm
if (bypass === "disabled") free.state.snapEnabled = false
free.startDrag("current_speed", largePoint(640, 30))
free.endDrag(largePoint(34, 430, { altKey: bypass === "alt" }))
assert.equal(free.layout.current_speed.x, 34)
assert.deepEqual(free.state.snapGuides, [])
}
snapVm.startDrag("current_speed", largePoint(640, 30))
snapVm.moveDrag(largePoint(34, 430))
assert.ok(snapVm.state.snapGuides.length)
snapVm.cancelDrag()
assert.deepEqual(snapVm.layout.current_speed, snapshot().document.layouts.large.current_speed)
assert.deepEqual(snapVm.state.snapGuides, [])
snapVm.changePosition("current_speed", 34, 430)
snapVm.onKey("current_speed", { key: "ArrowLeft", preventDefault() {} })
assert.equal(snapVm.layout.current_speed.x, 33)
snapVm.onKey("current_speed", { key: "ArrowRight", shiftKey: true, preventDefault() {} })
assert.equal(snapVm.layout.current_speed.x, 43)
assert.deepEqual(snapVm.state.snapGuides, [])
for (const pointerType of ["mouse", "touch"]) {
const phone = editor().vm
phone.$refs.preview.getBoundingClientRect = () => ({ left: 0, top: 0, width: 360, height: 180 })
for (const id of Object.keys(phone.layout)) if (id !== "current_speed") phone.state.hiddenPreviewWidgets.large[id] = true
const phonePoint = (x, y) => ({ ...snappedDrag.event(x / 6, y / 6), pointerType })
phone.startDrag("current_speed", phonePoint(640, 30))
phone.endDrag(phonePoint(75, 430))
assert.equal(phone.layout.current_speed.x, pointerType === "touch" ? 30 : 75)
}
const targetVisibility = editor().vm
targetVisibility.profile.widgets.pip = { ...widget("Side camera", "pip_camera", 60, 60, 400, 300), layer: "underlay" }
targetVisibility.layout.pip = { x: 400, y: 300, enabled: true }
targetVisibility.startDrag("current_speed", largePoint(640, 30))
targetVisibility.moveDrag(largePoint(464, 430))
assert.equal(targetVisibility.layout.current_speed.x, 460)
assert.ok(targetVisibility.state.snapGuides.some(guide => guide.label === "Side camera"))
targetVisibility.cancelDrag()
targetVisibility.togglePreviewVisibility("pip")
targetVisibility.startDrag("current_speed", largePoint(640, 30))
targetVisibility.moveDrag(largePoint(464, 430))
assert.equal(targetVisibility.layout.current_speed.x, 464)
targetVisibility.cancelDrag()
targetVisibility.startDrag("current_speed", largePoint(640, 30))
targetVisibility.moveDrag(largePoint(443, 430))
assert.equal(targetVisibility.layout.current_speed.x, 443, "torque arc footprint must not attract other widgets")
targetVisibility.cancelDrag()
const invalidSnap = editor().vm
invalidSnap.selectProfile("compact")
invalidSnap.$refs.preview.getBoundingClientRect = () => ({ left: 0, top: 0, width: 536, height: 240 })
invalidSnap.state.hiddenPreviewWidgets.compact.speed_limit_actions = true
const smallPoint = (x, y) => ({ ...snappedDrag.event(x, y), pointerType: "mouse" })
invalidSnap.startDrag("max_speed", smallPoint(0, 0))
invalidSnap.moveDrag(smallPoint(100, 18)) // Speed limit's top attracts to y=20, overlapping protected actions.
assert.equal(clampPlacement(invalidSnap.profile, "max_speed", 100, 20, invalidSnap.layout), null)
assert.deepEqual(invalidSnap.layout.max_speed, { x: 100, y: 18, enabled: true })
assert.deepEqual(invalidSnap.state.snapGuides, [])
assert.equal(invalidSnap.state.placementError, "")
invalidSnap.moveDrag(smallPoint(100, 100))
assert.deepEqual(invalidSnap.layout.max_speed, { x: 100, y: 18, enabled: true })
assert.ok(invalidSnap.state.placementError)
invalidSnap.cancelDrag()
const traySnap = editor().vm
traySnap.layout.current_speed.enabled = false
traySnap.startDrag("current_speed", largePoint(2400, 0), true)
traySnap.moveDrag(largePoint(324, 550)) // Widget center follows the tray pointer.
assert.equal(traySnap.layout.current_speed.x, 30)
assert.equal(traySnap.layout.current_speed.enabled, true)
assert.ok(traySnap.state.snapGuides.length)
traySnap.moveDrag(largePoint(2400, 550))
assert.equal(traySnap.layout.current_speed.enabled, false)
assert.deepEqual(traySnap.state.snapGuides, [])
traySnap.cancelDrag()
assert.equal(traySnap.layout.current_speed.enabled, false)
const clampedSnap = editor().vm
Object.assign(clampedSnap.profile.widgets.driver_monitor, { expandable: false, bounds: { x: 30, y: 30, width: 300, height: 1020 } })
clampedSnap.layout.current_speed.x = 184
clampedSnap.startDrag("driver_monitor", largePoint(88, 808))
clampedSnap.moveDrag(largePoint(187, 400)) // Current speed at 184 is beyond the mover's own bounds.
assert.equal(clampedSnap.layout.driver_monitor.x, 138)
assert.deepEqual(clampedSnap.state.snapGuides.filter(guide => guide.axis === "x"),
[{ axis: "x", value: 330, kind: "bounds", label: "the edge" }], "a clamped widget reports the edge it rests on, not the target it missed")
clampedSnap.moveDrag(largePoint(400, 400)) // Pushed far past the edge, it still reports resting on it.
assert.equal(clampedSnap.layout.driver_monitor.x, 138)
assert.ok(clampedSnap.state.snapGuides.some(guide => guide.axis === "x" && guide.value === 330 && guide.kind === "bounds"))
clampedSnap.cancelDrag()
const resizeSnap = editor().vm
resizeSnap.profile.widgets.map = { ...widget("Map", "nav_map", 200, 200, 300, 300),
box: { minWidth: 100, maxWidth: 1000, minHeight: 100, maxHeight: 1000 } }
resizeSnap.layout.map = { x: 300, y: 300, width: 200, height: 200, enabled: true }
const resizePoint = (x, y, options = {}) => ({ ...largePoint(x, y, options), stopPropagation() {} })
resizeSnap.startResize("map", resizePoint(500, 500))
resizeSnap.moveDrag(resizePoint(564, 671))
assert.deepEqual([resizeSnap.layout.map.width, resizeSnap.layout.map.height], [268, 375])
assert.deepEqual([resizeSnap.layout.map.x, resizeSnap.layout.map.y], [300, 300])
assert.equal(resizeSnap.state.snapGuides.length, 2)
resizeSnap.endDrag(resizePoint(564, 671))
assert.deepEqual(resizeSnap.state.snapGuides, [])
assert.equal(resizeSnap.state.history.undo.length, 1)
resizeSnap.undo()
resizeSnap.startResize("map", resizePoint(500, 500))
resizeSnap.moveDrag(resizePoint(564, 671, { altKey: true }))
assert.deepEqual([resizeSnap.layout.map.width, resizeSnap.layout.map.height], [264, 371])
assert.deepEqual(resizeSnap.state.snapGuides, [])
resizeSnap.cancelDrag()
resizeSnap.profile.widgets.map.box.maxWidth = 250
resizeSnap.startResize("map", resizePoint(500, 500))
resizeSnap.moveDrag(resizePoint(564, 671))
assert.equal(resizeSnap.layout.map.width, 250)
assert.deepEqual(resizeSnap.state.snapGuides, [])
resizeSnap.cancelDrag()
const originalStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage"), preferences = new Map()
try {
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: {
getItem: key => preferences.get(key) ?? null, setItem: (key, value) => preferences.set(key, value)
} })
const preference = editor().vm
assert.equal(preference.state.snapEnabled, true)
preference.toggleSnap()
assert.equal(preferences.get("gx-layout-snap-v1"), "0")
assert.equal(editor().vm.state.snapEnabled, false)
assert.equal(OnroadLayoutPage.setup({ projection: true }).state.snapEnabled, false)
preference.toggleSnap()
assert.equal(preferences.get("gx-layout-snap-v1"), "1")
assert.equal(editor().vm.state.snapEnabled, true)
Object.defineProperty(globalThis, "localStorage", { configurable: true, get() { throw new Error("Storage blocked") } })
const blocked = editor().vm
assert.equal(blocked.state.snapEnabled, true)
blocked.toggleSnap()
assert.equal(blocked.state.snapEnabled, false)
} finally {
if (originalStorage) Object.defineProperty(globalThis, "localStorage", originalStorage)
else delete globalThis.localStorage
}
for (const [guides, expected] of [
[[{ axis: "x", kind: "center" }, { axis: "y", kind: "center" }], "Centered on the screen"],
[[{ axis: "x", kind: "center" }], "Centered across"],
[[{ axis: "y", kind: "center" }], "Centered top to bottom"],
[[{ label: "Home" }, { label: "Home" }, { label: "Work" }], "Lined up with Home and Work"],
[[{ label: "the edge" }, { label: "Home" }], "At the edge, lined up with Home"],
[[{ axis: "x", kind: "center", label: "the center" }, { axis: "y", kind: "bounds", label: "the edge" }], "Centered across, at the edge"],
[[{ axis: "y", kind: "center", label: "the center" }, { axis: "x", kind: "widget", label: "Home" }, { axis: "y", kind: "widget", label: "Work" }],
"Centered top to bottom, lined up with Home"],
]) {
snapVm.state.snapGuides = guides
assert.equal(snapVm.snapSummary, expected)
}
for (const clear of [() => snapVm.selectProfile("compact")]) {
snapVm.state.snapGuides = [{ axis: "x", value: 30, kind: "bounds", label: "the edge" }]
clear()
assert.deepEqual(snapVm.state.snapGuides, [])
}
console.log("Layout snapping integration: drag, touch, targets, fallback, resize, history, preference and keyboard passed")
// Layer visibility is local to the arrangement preview, including required widgets.
for (const projection of [false, true]) {
const visibility = editor().vm
visibility.projection = projection
const original = copy(visibility.state.draft)
const order = visibility.layerWidgets.map(widget => widget.id)
visibility.profile.widgets.current_speed.required = true
visibility.togglePreviewVisibility("current_speed")
assert.equal(visibility.previewHidden("current_speed"), true)
assert.equal(visibility.renderWidgets.some(widget => widget.id === "current_speed"), false)
assert.deepEqual(visibility.layerWidgets.map(widget => widget.id), order)
assert.deepEqual(visibility.state.draft, original)
assert.equal(visibility.dirty, false)
assert.equal(visibility.canUndo, false)
visibility.state.profile = "compact"
assert.equal(visibility.previewHidden("current_speed"), false)
visibility.state.profile = "large"
visibility.togglePreviewVisibility("current_speed")
assert.equal(visibility.renderWidgets.some(widget => widget.id === "current_speed"), true)
assert.deepEqual(visibility.state.draft, original)
}
const dmEditor = editor().vm
dmEditor.changePosition("driver_monitor", 700, 600)
dmEditor.selectProfile("compact")
dmEditor.changePosition("driver_monitor", 90, 70)
assert.deepEqual(dmEditor.state.draft.layouts.large.driver_monitor, { x: 700, y: 600, enabled: true })
assert.deepEqual(dmEditor.layout.driver_monitor, { x: 90, y: 70, enabled: true })
dmEditor.changePosition("driver_monitor", 200, 190)
assert.deepEqual(dmEditor.layout.driver_monitor, { x: 90, y: 70, enabled: true })
dmEditor.remove("driver_monitor")
assert.equal(dmEditor.layout.driver_monitor.enabled, false)
dmEditor.add("driver_monitor")
assert.equal(dmEditor.layout.driver_monitor.enabled, true)
dmEditor.resetLayout()
assert.deepEqual(dmEditor.layout.driver_monitor, { x: 16, y: 10, enabled: true })
assert.equal(dmEditor.state.draft.layouts.large.driver_monitor.x, 700)
const actionEditor = editor().vm
actionEditor.changePosition("speed_limit_actions", 388, 600)
assert.deepEqual(actionEditor.layout.speed_limit_actions, { x: 388, y: 600, enabled: true })
actionEditor.selectProfile("compact")
actionEditor.changePosition("speed_limit_actions", 100, 180)
assert.deepEqual(actionEditor.layout.speed_limit_actions, { x: 100, y: 180, enabled: true })
actionEditor.changePosition("speed_limit_actions", 10, 180)
assert.equal(actionEditor.layout.speed_limit_actions.x, 100)
assert.equal(validDocument(actionEditor.state.draft, actionEditor.state.data.metadata), true)
const wheelEditor = editor().vm
wheelEditor.state.selected = "steering_wheel"
wheelEditor.resizeSelected({ target: { value: "240" } })
assert.equal(wheelEditor.layout.steering_wheel.size, 240)
assert.equal(wheelEditor.renderWidgets.find((widget) => widget.id === "steering_wheel").width, 240)
wheelEditor.undo()
assert.equal(wheelEditor.layout.steering_wheel.size, 192)
wheelEditor.redo()
assert.equal(wheelEditor.layout.steering_wheel.size, 240)
wheelEditor.selectProfile("compact")
wheelEditor.state.selected = "steering_wheel"
wheelEditor.resizeSelected({ target: { value: "70" } })
assert.deepEqual(wheelEditor.layout.steering_wheel, { x: 21, y: 170, enabled: true, size: 70 })
assert.equal(wheelEditor.state.draft.layouts.large.steering_wheel.size, 240)
wheelEditor.resetLayout()
assert.equal(wheelEditor.layout.steering_wheel.size, 50)
assert.equal(wheelEditor.state.draft.layouts.large.steering_wheel.size, 240)
const dragWheel = editor()
dragWheel.vm.state.selected = "steering_wheel"
dragWheel.vm.resizeSelected({ target: { value: "240" } })
dragWheel.vm.startDrag("steering_wheel", dragWheel.event(954, 197.5))
dragWheel.vm.moveDrag(dragWheel.event(964, 197.5))
dragWheel.vm.endDrag(dragWheel.event(964, 197.5))
assert.deepEqual(dragWheel.vm.layout.steering_wheel, { x: 1590, y: 75, enabled: true, size: 240 })
assert.equal(dragWheel.captures.size, 0)
const torqueEditor = editor().vm
torqueEditor.changePosition("torque_bar", 500, 650)
torqueEditor.selectProfile("compact")
torqueEditor.changePosition("torque_bar", 174, 179)
assert.deepEqual(torqueEditor.renderWidgets.map(widget => widget.id), torqueEditor.layerWidgets.map(widget => widget.id).reverse())
assert.equal(torqueEditor.state.draft.layouts.large.torque_bar.x, 500)
assert.equal(torqueEditor.layout.torque_bar.y, 179)
torqueEditor.remove("torque_bar")
assert.equal(torqueEditor.layout.torque_bar.enabled, false)
torqueEditor.add("torque_bar")
assert.equal(torqueEditor.layout.torque_bar.enabled, true)
const { vm, event, emitted, calls, backdropCalls, captures } = editor()
// The comma's real screen loads behind the arrangement with the editor.
assert.deepEqual(backdropCalls.map((call) => Array.isArray(call) ? call[0] : call).slice(-2), ["start", "update"])
assert.deepEqual(backdropCalls.at(-1).slice(2, 5), ["large", "general", false])
assert.equal(vm.dirty, false)
vm.changePosition("current_speed", 700, 200)
vm.selectProfile("compact")
vm.changePosition("max_speed", 200, 18)
vm.state.selected = "driver_monitor"
vm.color("cardFill", "#112233aa")
vm.resetLayout()
assert.equal(vm.layout.max_speed.x, 0)
assert.equal(vm.state.draft.layouts.large.current_speed.x, 700)
assert.equal(vm.selectedColors.cardFill, "#112233AA")
vm.changePosition("max_speed", 200, 18)
vm.resetColors()
assert.equal(vm.selectedColors.cardFill, "#00000000")
assert.equal(vm.layout.max_speed.x, 200)
vm.changePosition("max_speed", 200, 65)
assert.equal(vm.layout.max_speed.y, 18)
assert.match(vm.state.placementError, /last valid position/)
const compactPoint = (x, y) => event(100 + x * 1080 / 536, 100 + y * 540 / 240)
vm.startDrag("speed_limit", compactPoint(350, 40))
vm.moveDrag(compactPoint(350, 100))
assert.equal(vm.layout.speed_limit.y, 80) // Intentional sign/action overlap survives dragging.
vm.endDrag(compactPoint(350, 100))
assert.equal(vm.layout.speed_limit.y, 80)
assert.equal(vm.state.drag, null)
vm.selectProfile("large")
assert.equal(vm.layout.current_speed.x, 700)
vm.onKey("current_speed", { key: "ArrowRight", shiftKey: true, preventDefault() {} })
assert.equal(vm.layout.current_speed.x, 710)
vm.onKey("current_speed", { key: "Delete", preventDefault() {} })
assert.equal(vm.state.removeConfirm, "current_speed")
assert.equal(vm.layout.current_speed.enabled, true)
vm.confirmRemove()
assert.equal(vm.layout.current_speed.enabled, false)
assert.equal(vm.inactiveWidgets.length, 1)
vm.startDrag("current_speed", event(1300, 100), true)
vm.moveDrag(event(500, 300))
assert.equal(vm.layout.current_speed.enabled, true)
assert.equal(vm.inactiveWidgets.length, 1) // Keep the captured tray element mounted during the drag.
assert.equal(vm.state.drag.fromTray, true)
assert.deepEqual({ ...vm.layout.current_speed }, { x: 510, y: 250, enabled: true })
assert.equal(captures.has(1), true)
vm.endDrag(event(500, 300))
assert.equal(captures.size, 0)
assert.equal(vm.inactiveWidgets.length, 0)
vm.startDrag("current_speed", event(400, 250))
vm.moveDrag(event(2000, 2000))
assert.deepEqual({ ...vm.layout.current_speed }, { x: 1250, y: 750, enabled: true })
vm.cancelDrag()
assert.deepEqual({ ...vm.layout.current_speed }, { x: 510, y: 250, enabled: true })
vm.remove("current_speed")
vm.startDrag("current_speed", event(1300, 100), true)
vm.moveDrag(event(500, 300, 2)) // A second pointer cannot change this drag.
assert.equal(vm.layout.current_speed.enabled, false)
vm.endDrag(event(1300, 100))
assert.equal(vm.layout.current_speed.enabled, false)
vm.add("current_speed")
assert.deepEqual({ ...vm.layout.current_speed }, { x: 510, y: 250, enabled: true })
vm.color("text", "invalid")
assert.match(vm.state.colorError, /eight hex digits/)
assert.equal(vm.selectedColors.text, "#FFFFFFFF")
vm.colorRgb("text", { target: { value: "#abcdef" } })
vm.colorAlpha("text", { target: { value: "128" } })
assert.equal(vm.selectedColors.text, "#ABCDEF80")
// Driving scenes do not leave a CEM editing placeholder behind.
assert.doesNotMatch(OnroadLayoutPage.template, /data-preview-source="cem-hint"/)
// The Small screen shows the tap-areas chip under the layout instead of over it.
assert.match(OnroadLayoutPage.template, /state\.profile !== 'compact'" class="gx-layout__stage-chip" aria-hidden="true"><i class="bi bi-grid-3x3-gap"><\/i>Tap areas/)
assert.match(OnroadLayoutPage.template, /state\.profile === 'compact'" class="gx-layout__stage-chip gx-layout__stage-chip--below"/)
// One scene dropdown, grouped, with every scene exactly once.
assert.deepEqual(vm.sceneGroups.map(group => group.label), ["Editing", "Driving", "Conditional experimental", "Speed limit"])
assert.deepEqual(vm.sceneGroups.flatMap(group => group.scenes.map(scene => scene.id)).sort(), vm.scenes.map(scene => scene.id).sort())
// The Small screen only offers scenes that look different there; a hidden pick falls back to General edit.
vm.state.scene = "slc_display"
vm.selectProfile("compact")
assert.equal(vm.state.scene, "general")
const smallScenes = vm.sceneGroups.flatMap(group => group.scenes.map(scene => scene.id))
for (const id of ["slc_pedal", "slc_display"]) assert.ok(!smallScenes.includes(id), id)
assert.ok(smallScenes.includes('slc_retained')) // The override X is visible on the Small screen too.
assert.ok(smallScenes.includes("slc_unavailable") && smallScenes.includes("slc_disengaged"))
vm.selectProfile("large")
assert.equal(vm.sceneGroups.flatMap(group => group.scenes).length, vm.scenes.length)
assert.match(OnroadLayoutPage.template, /<GalaxySelect class="gx-field gx-field--full" aria-label="Preview scene"/)
// The widget open in Edit glows; the sources panel is a layout setting, not an on-screen toggle.
assert.match(OnroadLayoutPage.template, /'is-editing': state\.showEditingAids && state\.inspectorPanel === 'edit' && state\.selected === widget\.id/)
// Sources sit inside the cruise card (or a card right under it), so its box grows and actions right below follow.
vm.state.data.metadata.profiles.large.widgets.cruise_limits.sourcesExtent = { inline: 76, card: 84 }
vm.state.selected = "cruise_limits"
const sourcesCard = vm.layout.cruise_limits, sourcesActions = vm.layout.speed_limit_actions
const actionsBefore = { ...sourcesActions }
const sourceDx = vm.profile.widgets.speed_limit_actions.default.x - vm.profile.widgets.cruise_limits.default.x
const sourceGap = vm.profile.widgets.speed_limit_actions.default.y - vm.profile.widgets.cruise_limits.default.y - 522
Object.assign(sourcesActions, { x: sourcesCard.x + sourceDx, y: sourcesCard.y + 522 + sourceGap })
vm.setSpeedSources("speedSources", true)
assert.equal(vm.state.draft.speedSources, true)
assert.equal(vm.profile.widgets.cruise_limits.height, 522 + 76)
assert.equal(sourcesActions.y, sourcesCard.y + 522 + 76 + sourceGap)
vm.setSpeedSources("speedSourcesCard", true)
assert.equal(vm.profile.widgets.cruise_limits.height, 522 + 84)
assert.equal(sourcesActions.y, sourcesCard.y + 522 + 84 + sourceGap)
vm.undo(); vm.undo()
assert.notEqual(vm.state.draft.speedSources, true)
assert.equal(vm.profile.widgets.cruise_limits.height, 522)
assert.equal(vm.layout.speed_limit_actions.y, vm.layout.cruise_limits.y + 522 + sourceGap)
Object.assign(vm.layout.speed_limit_actions, actionsBefore)
vm.state.selected = "current_speed"
vm.setSpeedSources("speedSources", true)
assert.notEqual(vm.state.draft.speedSources, true) // Only from the cruise widget's own panel.
vm.state.selected = "cruise_limits"
assert.doesNotMatch(OnroadLayoutPage.template, /speedAlign|Text alignment/)
assert.equal(OnroadLayoutPage.methods.setSpeedAlign, undefined)
vm.setSpeedSources("palette", true)
assert.equal(vm.state.draft.palette === true, false) // Only the two sources settings.
// Moving or removing widgets keeps the same backdrop; scene and size changes replace it.
const backdropCount = backdropCalls.length
vm.changePosition("current_speed", 720, 220)
vm.changePosition("current_speed", 510, 250)
vm.remove("current_speed")
vm.add("current_speed")
assert.equal(backdropCalls.length, backdropCount)
vm.selectProfile("compact")
assert.equal(backdropCalls.at(-1)[2], "compact")
vm.state.scene = "braking"
assert.equal(backdropCalls.at(-1)[3], "braking")
vm.selectProfile("large")
vm.state.scene = "engaged"
vm.color("text", "#ABCDEF81")
assert.equal(backdropCalls.length, backdropCount + 5) // Color edits change the real screen too.
vm.color("text", "#ABCDEF80")
const draggingCount = backdropCalls.length
vm.startDrag("current_speed", event(500, 200))
vm.moveDrag(event(700, 250))
vm.cancelDrag()
assert.equal(backdropCalls.length, draggingCount)
vm.state.data.editable = false
assert.equal(backdropCalls.at(-1), "stop")
vm.state.data.editable = true
vm.requestLeave("reload")
assert.equal(vm.state.discard, "reload")
assert.equal(calls.length, 0)
assert.equal(vm.editable, false)
vm.state.discard = null
vm.save()
assert.equal(calls.length, 1)
assert.equal(calls[0].layouts.compact.max_speed.x, 200)
vm.requestLeave("back")
assert.equal(emitted.length, 0)
vm.leave("back")
assert.deepEqual(emitted, [["close"]])
vm.state.data.editable = false
const parkedDraft = copy(vm.state.draft)
vm.changePosition("current_speed", 99, 99); vm.remove(); vm.resetLayout(); vm.resetColors(); vm.color("text", "#00000000"); vm.save()
assert.deepEqual(copy(vm.state.draft), parkedDraft)
assert.equal(calls.length, 1)
const historyEditor = editor()
const history = historyEditor.vm
assert.equal(history.canUndo, false)
assert.equal(history.stockChanged, false)
history.resetToStock()
assert.equal(history.state.history.undo.length, 0)
history.remove("current_speed")
history.changePosition("current_speed", 700, 200) // Disabled widgets keep editable positions.
assert.equal(history.state.history.undo.length, 2)
history.undo()
assert.deepEqual(copy(history.layout.current_speed), { x: 640, y: 30, enabled: false })
history.undo()
assert.deepEqual(copy(history.layout.current_speed), { x: 640, y: 30, enabled: true })
history.redo()
history.redo()
assert.deepEqual(copy(history.layout.current_speed), { x: 700, y: 200, enabled: false })
history.selectProfile("compact")
history.changePosition("max_speed", 200, 18)
history.undo()
assert.equal(history.layout.max_speed.x, 0)
assert.equal(history.state.draft.layouts.large.current_speed.x, 700)
history.redo()
assert.equal(history.layout.max_speed.x, 200)
const dragEditor = editor()
const dragged = dragEditor.vm
const beforeDrag = copy(dragged.state.draft)
dragged.startDrag("current_speed", dragEditor.event(500, 150))
dragged.moveDrag(dragEditor.event(600, 200))
dragged.moveDrag(dragEditor.event(700, 250))
assert.equal(dragged.state.history.undo.length, 0)
dragged.undo() // History cannot replace a draft during an active gesture.
assert.equal(dragged.state.drag.id, "current_speed")
dragged.endDrag(dragEditor.event(700, 250))
assert.equal(dragged.state.history.undo.length, 1)
const afterDrag = copy(dragged.state.draft)
dragged.undo()
assert.deepEqual(copy(dragged.state.draft), beforeDrag)
dragged.redo()
assert.deepEqual(copy(dragged.state.draft), afterDrag)
dragged.startDrag("current_speed", dragEditor.event(700, 250))
dragged.moveDrag(dragEditor.event(800, 300))
dragged.cancelDrag()
assert.deepEqual(copy(dragged.state.draft), afterDrag)
assert.equal(dragged.state.history.undo.length, 1)
assert.equal(dragged.state.history.redo.length, 0)
dragged.remove("current_speed")
const beforeTray = copy(dragged.state.draft)
const historySize = dragged.state.history.undo.length
dragged.startDrag("current_speed", dragEditor.event(1300, 100), true)
dragged.moveDrag(dragEditor.event(500, 300))
dragged.endDrag(dragEditor.event(500, 300))
assert.equal(dragged.state.history.undo.length, historySize + 1)
dragged.undo()
assert.deepEqual(copy(dragged.state.draft), beforeTray)
const colorEditor = editor().vm
colorEditor.colorAlpha("text", { target: { value: "200" } })
colorEditor.colorAlpha("text", { target: { value: "180" } })
colorEditor.colorAlpha("text", { target: { value: "160" } })
assert.equal(colorEditor.state.history.undo.length, 1)
colorEditor.undo()
assert.equal(colorEditor.selectedColors.text, "#FFFFFFFF")
colorEditor.redo()
assert.equal(colorEditor.selectedColors.text, "#FFFFFFA0")
colorEditor.finishColorEdit()
colorEditor.colorRgb("text", { target: { value: "#abcdef" } })
colorEditor.colorRgb("text", { target: { value: "#123456" } })
assert.equal(colorEditor.state.history.undo.length, 2)
colorEditor.undo()
assert.equal(colorEditor.selectedColors.text, "#FFFFFFA0")
colorEditor.colorText("text", { target: { value: "#01020304" } })
assert.equal(colorEditor.canRedo, false) // A fresh edit replaces the redo branch.
const stockFixture = editor()
const stockEditor = stockFixture.vm
stockEditor.changePosition("current_speed", 700, 200)
stockEditor.remove("driver_monitor")
stockEditor.changePosition("driver_monitor", 100, 700)
stockEditor.selectProfile("compact")
stockEditor.changePosition("max_speed", 200, 18)
stockEditor.state.selected = "driver_monitor"
stockEditor.color("cardFill", "#11223344")
const beforeStock = copy(stockEditor.state.draft)
const beforeStockHistory = stockEditor.state.history.undo.length
stockEditor.resetToStock()
assert.deepEqual(copy(stockEditor.state.draft), stockEditor.state.data.defaults)
assert.equal(stockEditor.state.history.undo.length, beforeStockHistory + 1)
assert.match(stockEditor.state.notice, /Save to apply/)
stockEditor.undo()
assert.deepEqual(copy(stockEditor.state.draft), beforeStock)
stockEditor.redo()
assert.deepEqual(copy(stockEditor.state.draft), stockEditor.state.data.defaults)
stockFixture.publish({ status: "ready", data: { ...snapshot(), document: copy(stockEditor.state.draft) },
draft: copy(stockEditor.state.draft), notice: "Colors and both layouts saved." })
assert.equal(stockEditor.canUndo, false)
assert.equal(stockEditor.canRedo, false)
stockEditor.changePosition("max_speed", 100, 18)
assert.equal(stockEditor.canUndo, true)
stockFixture.publish({ status: "ready", error: "Reload failed." })
assert.equal(stockEditor.canUndo, true) // Failed reads keep the current draft and history.
stockFixture.publish({ status: "ready", data: snapshot(), draft: copy(snapshot().document) })
assert.equal(stockEditor.canUndo, false)
assert.equal(stockEditor.canRedo, false)
stockEditor.state.data.editable = false
stockEditor.changePosition("max_speed", 200, 18)
stockEditor.undo()
stockEditor.resetToStock()
assert.deepEqual(copy(stockEditor.state.draft), snapshot().document)
assert.match(OnroadLayoutPage.template, /Reset to stock StarPilot/)
assert.match(OnroadLayoutPage.template, /@click="undo"/)
assert.match(OnroadLayoutPage.template, /@click="redo"/)
assert.match(OnroadLayoutPage.template, /class="gx-layout__remove gx-icon-btn"/)
assert.match(OnroadLayoutPage.template, /Remove widget/)
assert.doesNotMatch(OnroadLayoutPage.template, />Remove from layout</)
assert.match(OnroadLayoutPage.template, /:clock24-hour="!!state\.draft\.clock24Hour"/)
assert.match(LayoutWidgetPreview.template, /clock24Hour \? '18:00' : '12:00 PM'/)
const flush = async () => { for (let i = 0; i < 10; i++) await Promise.resolve() }
function feedFixture() {
const requests = [], updates = [], timers = new Map(); let nextTimer = 0, unauthorized = 0
const feed = new OnroadLayoutFeed({ publish: (value) => updates.push(value), unauthorized: () => { unauthorized++ },
fetcher: (url, options) => new Promise((resolve) => requests.push({ url, options, resolve })),
later: (fn, ms) => { fn.delay = ms; timers.set(++nextTimer, fn); return nextTimer }, cancelTimer: (id) => timers.delete(id) })
async function reply(index, body = snapshot(), status = 200) {
requests[index].resolve({ ok: status === 200, status, json: async () => body }); await flush()
}
function fire(ms) {
const entry = [...timers.entries()].find(([, fn]) => fn.delay === ms)
if (!entry) return false
timers.delete(entry[0]); entry[1](); return true
}
return { feed, requests, updates, timers, reply, fire, get unauthorized() { return unauthorized } }
}
const normal = feedFixture()
normal.feed.start()
assert.equal(normal.requests[0].url, "./api/ui/layout")
const legacyAlignment = snapshot()
legacyAlignment.document.speedAlign = "right"
await normal.reply(0, legacyAlignment)
const draft = copy(normal.feed.data.document); draft.layouts.compact.max_speed.x = 80
normal.feed.save(draft)
normal.feed.load(); normal.feed.save(draft)
assert.equal(normal.requests.length, 2) // No load or second save may race a dispatched save.
assert.deepEqual(JSON.parse(normal.requests[1].options.body), { revision: "saved-revision", document: draft })
assert.equal(normal.requests[1].options.credentials, "same-origin")
await normal.reply(1, { ...snapshot(), document: draft, revision: "new-revision" })
assert.equal(normal.updates.at(-1).draft.layouts.compact.max_speed.x, 80)
assert.equal(normal.updates.at(-1).draft.speedAlign, "right") // Unrelated edits preserve legacy layout fields.
assert.equal(normal.feed.data.revision, "new-revision")
for (const status of [409, 503, 400]) {
const conflict = feedFixture(); conflict.feed.start(); await conflict.reply(0)
conflict.feed.save(draft); await conflict.reply(1, {}, status)
assert.equal(conflict.feed.needsReload, true)
assert.equal(conflict.updates.at(-1).draft, undefined) // Failure never replaces the unsaved document.
conflict.feed.save(draft); assert.equal(conflict.requests.length, 2)
conflict.feed.load(); await conflict.reply(2)
assert.equal(conflict.feed.needsReload, false)
}
const timeout = feedFixture(); timeout.feed.start(); await timeout.reply(0); timeout.feed.save(draft)
const expire = timeout.timers.get(timeout.feed.timer); expire()
assert.match(timeout.updates.at(-1).error, /result is unknown/)
assert.equal(timeout.requests[1].options.signal.aborted, true)
await timeout.reply(1, { ...snapshot(), document: draft })
assert.equal(timeout.feed.data.document.layouts.compact.max_speed.x, 0)
const late = feedFixture(); late.feed.start(); late.feed.stop(); await late.reply(0)
assert.equal(late.feed.data, null)
const invalid = feedFixture(); invalid.feed.start(); await invalid.reply(0, { ...snapshot(), editable: "yes" })
assert.equal(invalid.feed.data, null)
assert.equal(invalid.updates.at(-1).status, "unavailable")
const unauthorized = feedFixture(); unauthorized.feed.start(); await unauthorized.reply(0, {}, 401)
assert.equal(unauthorized.unauthorized, 1)
const settling = feedFixture()
settling.feed.start(); await settling.reply(0, { ...snapshot(), editable: false })
assert.equal(settling.updates.at(-1).data.editable, false)
assert.equal(settling.fire(5000), true)
assert.equal(settling.requests.length, 2)
await settling.reply(1, { ...snapshot(), editable: true })
assert.equal(settling.updates.at(-1).data.editable, true)
assert.equal([...settling.timers.values()].some((fn) => fn.delay === 5000), true)
const stoppedRetry = feedFixture()
stoppedRetry.feed.start(); await stoppedRetry.reply(0, { ...snapshot(), editable: false })
const pendingRetry = [...stoppedRetry.timers.values()].find((fn) => fn.delay === 5000)
stoppedRetry.feed.stop(); pendingRetry()
assert.equal(stoppedRetry.requests.length, 1)
const manualRetry = feedFixture()
manualRetry.feed.start(); await manualRetry.reply(0, { ...snapshot(), editable: false })
const cancelledRetry = [...manualRetry.timers.values()].find((fn) => fn.delay === 5000)
manualRetry.feed.load()
cancelledRetry()
assert.equal(manualRetry.requests.length, 2)
await manualRetry.reply(1, snapshot())
assert.equal(manualRetry.updates.at(-1).data.editable, true)
const boundedRetry = feedFixture()
boundedRetry.feed.start()
for (let index = 0; index < 4; index++) {
await boundedRetry.reply(index, { ...snapshot(), editable: false })
boundedRetry.fire(5000)
}
assert.equal(boundedRetry.requests.length, 5)
boundedRetry.feed.stop()
assert.equal([...boundedRetry.timers.values()].some((fn) => fn.delay === 5000), false)
const visual = SETTINGS_SECTIONS.find(({ id }) => id === "visual")
const rows = SettingsPage.computed.visibleRows.call({ initialPage: "hub", activeSection: visual,
state: { data: { page: "hub" }, query: "" } })
assert.equal(rows.filter(({ row }) => row.page === "ui_layout").length, 1)
const navigated = [], host = { initialPage: "hub", state: {}, feed: { stop: () => navigated.push("stop"), load: () => assert.fail("Layout is not a generic settings endpoint"), start: (page) => navigated.push(page) } }
SettingsPage.methods.open.call(host, "ui_layout")
assert.equal(host.state.layoutOpen, true)
SettingsPage.methods.closeLayout.call(host)
assert.equal(host.state.section, "visual")
assert.deepEqual(navigated, ["stop", "hub"])
host.initialPage = "appearance"
SettingsPage.methods.open.call(host, "ui_layout")
SettingsPage.methods.closeLayout.call(host)
assert.deepEqual(navigated.slice(-2), ["stop", "appearance"])
const appSource = readFileSync(new URL("../web/js/app.js", import.meta.url), "utf8")
const editorSource = readFileSync(new URL("../web/js/onroad-layout.js", import.meta.url), "utf8")
const catalog = JSON.parse(readFileSync(new URL("../web/data/catalog.json", import.meta.url), "utf8"))
assert.ok(appSource.includes("['/theme_maker', '/theme_maker/starport', '/appearance'].includes(route.path)"))
assert.ok(appSource.includes(':key="route.path" :projection="route.path ==='))
assert.ok(appSource.includes('@target="go($event ==='))
assert.match(appSource, /goFromMenu\(path\)/)
assert.match(appSource, /requestLeave\(proceed, presentation\)/)
assert.match(appSource, /@click="goFromMenu\(item\.path\)"/)
assert.equal(OnroadLayoutPage.template, ONROAD_LAYOUT_TEMPLATE, "the page uses the extracted template")
assert.equal(OnroadLayoutPage.components.LayoutWidgetPreview, LayoutWidgetPreview, "the template preview uses the actual component")
assert.match(OnroadLayoutPage.template, /<LayoutWidgetPreview :widget="widget"/)
assert.doesNotMatch(OnroadLayoutPage.template, /<LayoutWidgetPreview v-if="!state\.preview\.url"/)
assert.match(OnroadLayoutPage.template, /<image v-if="nativeBackdropReady" :href="state\.backdrop\.url"/)
assert.doesNotMatch(editorSource, /devicePreviewOpen|gx-layout__device|selectPreview/)
assert.doesNotMatch(OnroadLayoutPage.template, /devicePreviewOpen|gx-layout__device|selectPreview/)
const loadedCatalog = await loadCatalog({ fetcher: async (url) => ({ ok: true,
json: async () => url.endsWith("catalog.json") ? catalog : { schemaVersion: 1, monitor: "local" },
}) })
assert.ok(loadedCatalog.tools.some(tool => tool.path === "/theme_maker"))
assert.equal(catalog.tools.find((tool) => tool.path === "/theme_maker")?.availability, "local-only")
console.log("Colors and layout: stable editor, device backdrop, placement, drag/cancel, resets, save races, errors, auth and Visual navigation passed")
const railEditor = editor().vm
railEditor.selectProfile("compact")
railEditor.changePosition("conditional_mode", 260, 90)
assert.deepEqual(railEditor.layout.conditional_mode, { x: 260, y: 90, enabled: true })
railEditor.remove("conditional_mode")
assert(railEditor.inactiveWidgets.some(widget => widget.id === "conditional_mode"))
railEditor.add("conditional_mode")
assert(railEditor.activeWidgets.some(widget => widget.id === "conditional_mode"))
railEditor.undo()
assert.equal(railEditor.layout.conditional_mode.enabled, false)
railEditor.resetToStock()
assert.deepEqual(railEditor.layout.conditional_mode, { x: 476, y: 80, enabled: true })
const badRail = snapshot()
badRail.metadata.profiles.compact.widgets.model_confidence.bounds.width = 900
assert.equal(validSnapshot(badRail), false)
assert.equal(clampPlacement(data.metadata.profiles.compact, "conditional_mode", 260, 101), null)
const scoped = editor().vm
const legacyPalette = copy(scoped.state.draft.palette)
scoped.selectProfile('compact')
scoped.state.selected = 'conditional_mode'
assert.deepEqual(scoped.colorFields.map(({ id }) => id), ['cardFill', 'cardBorder'])
scoped.colorRgb('cardFill', { target: { value: '#224466' } })
assert.equal(scoped.selectedColors.cardFill, '#224466A6') // Picking a color reveals a transparent frame.
assert.deepEqual(copy(scoped.state.draft.palette), legacyPalette)
assert.equal(widgetPalette(scoped.state.draft, scoped.state.data.metadata, 'compact', 'model_confidence').cardFill, '#00000000')
scoped.state.selected = 'following_distance'
scoped.color('text', '#FEDCBAFF')
assert.equal(scoped.selectedColors.text, '#FEDCBAFF')
scoped.undo()
assert.equal(scoped.selectedColors.text, '#FFFFFFFF')
assert.equal(scoped.state.draft.widgetColors.compact.conditional_mode.cardFill, '#224466A6')
scoped.redo()
scoped.resetColors()
assert.equal(scoped.selectedColors.text, '#FFFFFFFF')
assert.equal(scoped.state.draft.widgetColors.compact.conditional_mode.cardFill, '#224466A6')
scoped.selectProfile('large')
scoped.state.selected = 'driver_monitor'
assert.equal(scoped.selectedColors.cardFill, '#00000000')
scoped.color('cardFill', '#11223344')
assert.equal(scoped.state.draft.widgetColors.compact.conditional_mode.cardFill, '#224466A6')
scoped.state.selected = 'torque_bar'
assert.equal(scoped.colorFields.length, 0)
const beforeUnsupported = copy(scoped.state.draft)
scoped.color('text', '#11223344')
assert.deepEqual(copy(scoped.state.draft), beforeUnsupported)
assert.equal(validDocument(scoped.state.draft, scoped.state.data.metadata), true)
for (const mutate of [
doc => { doc.widgetColors.large.current_speed = { cardFill: '#FFFFFFFF' } },
doc => { doc.widgetColors.compact.speed_limit = { text: '#FFFFFFFF' } },
doc => { doc.widgetColors.large.unknown = {} },
doc => { doc.widgetColors.compact = [] },
doc => { doc.widgetColors.compact.driver_monitor = { cardFill: true } },
]) {
const invalid = copy(scoped.state.draft); mutate(invalid)
assert.equal(validDocument(invalid, scoped.state.data.metadata), false)
}
const migrated = editor().vm
migrated.state.draft.palette.text = '#FF0000FF'
migrated.state.selected = 'current_speed'
assert.equal(migrated.selectedColors.text, '#FF0000FF')
migrated.resetColors()
assert.equal(migrated.selectedColors.text, '#FFFFFFFF')
assert.equal(widgetPalette(migrated.state.draft, migrated.state.data.metadata, 'large', 'cruise_limits').text, '#FF0000FF')
console.log('Widget colors: real fields, visible picks, profile/widget isolation, undo/reset, legacy inheritance and invalid fields passed')
const savedSelection = editor()
savedSelection.vm.selectProfile('compact')
savedSelection.vm.state.selected = 'speed_limit_actions'
savedSelection.vm.color('cardFill', '#12345680')
const savedData = snapshot()
savedData.document = copy(savedSelection.vm.state.draft)
savedSelection.publish({ status: 'ready', data: savedData, draft: copy(savedData.document) })
assert.equal(savedSelection.vm.state.selected, 'speed_limit_actions')
assert.equal(savedSelection.vm.selectedColors.cardFill, '#12345680')
const roadEditor = editor().vm
roadEditor.roadRgb('path', { target: { value: '#FF1122' } })
assert.equal(roadEditor.roadMode, 'color')
assert.equal(roadEditor.roadColors.path, '#FF1122FF')
roadEditor.roadAlpha('path', { target: { value: '128' } })
assert.equal(roadEditor.roadGradient[0].alpha, 128 / 255)
assert.equal(roadEditor.roadLabel('pathEdge'), 'Path border')
roadEditor.selectProfile('compact')
assert.equal(roadEditor.roadMode, 'acceleration')
assert.equal(roadEditor.roadLabel('pathEdge'), 'Closest lane markings')
roadEditor.setRoadMode('rainbow')
assert.equal(roadEditor.roadGradient.length, 12)
roadEditor.undo()
assert.equal(roadEditor.roadMode, 'acceleration')
roadEditor.redo()
assert.equal(roadEditor.roadMode, 'rainbow')
roadEditor.resetRoad()
assert.deepEqual(copy(roadEditor.state.draft.roadColors.compact), {})
assert.equal(roadEditor.state.draft.roadColors.large.path, '#FF112280')
roadEditor.roadColor('pathEdge', '#AABBCCFF')
assert.equal(validDocument(roadEditor.state.draft, roadEditor.state.data.metadata), true)
for (const mutate of [
doc => { doc.roadColors.large.pathMode = 'garbage' },
doc => { doc.roadColors.compact.path = 1 },
doc => { doc.roadColors.large = [] },
doc => { doc.roadColors.compact.fake = '#123456FF' },
]) {
const invalid = copy(roadEditor.state.draft); mutate(invalid)
assert.equal(validDocument(invalid, roadEditor.state.data.metadata), false)
}
roadEditor.resetToStock()
assert.deepEqual(copy(roadEditor.state.draft.roadColors), { large: {}, compact: {} })
console.log('Road colors: original defaults, custom/rainbow modes, alpha, profile separation, undo/reset and validation passed')
// Both drag directions permit the sign/action pair and persist the whole draft.
const overlap = editor().vm
const bigBefore = copy(overlap.state.draft.layouts.large)
overlap.selectProfile('compact')
overlap.changePosition('speed_limit', 330, 108)
assert.equal(overlap.layout.speed_limit.y, 108)
overlap.changePosition('speed_limit_actions', 174, 100)
assert.equal(overlap.layout.speed_limit_actions.y, 100)
assert.equal(validDocument(overlap.state.draft, overlap.state.data.metadata), true)
assert.deepEqual(copy(overlap.state.draft.layouts.large), bigBefore)
const originalOrder = overlap.renderWidgets.map(widget => widget.id)
overlap.state.selected = 'speed_limit'
assert.deepEqual(overlap.renderWidgets.map(widget => widget.id), originalOrder)
overlap.state.selected = 'speed_limit_actions'
assert.deepEqual(overlap.renderWidgets.map(widget => widget.id), originalOrder)
overlap.changePosition('speed_limit_actions', 174, 0)
assert.equal(overlap.layout.speed_limit_actions.y, 0)
assert.equal(validDocument(overlap.state.draft, overlap.state.data.metadata), true)
assert.equal(overlap.renderWidgets.find(widget => widget.id === "speed_limit_actions").visualHeaderY, 62)
assert.equal(overlap.renderWidgets.find(widget => widget.id === "speed_limit_actions").visualInsetTop, 0)
assert.equal(overlap.renderWidgets.find(widget => widget.id === "speed_limit_actions").visualInsetBottom, 32)
const synchronizedDrag = editor()
const syncVm = synchronizedDrag.vm
const dragDraft = syncVm.state.draft
syncVm.startDrag("current_speed", synchronizedDrag.event(500, 150))
syncVm.moveDrag(synchronizedDrag.event(600, 200))
const remote = snapshot()
remote.revision = "f".repeat(64)
synchronizedDrag.publish({ status: "ready", data: remote, draft: copy(remote.document) })
assert.equal(syncVm.state.draft, dragDraft)
assert.equal(syncVm.state.drag.id, "current_speed")
assert.equal(synchronizedDrag.captures.has(1), true)
syncVm.endDrag(synchronizedDrag.event(600, 200))
await Promise.resolve()
assert.equal(syncVm.state.draft, dragDraft)
assert.equal(syncVm.state.needsReload, true)
assert.equal(syncVm.state.history.undo.length, 1)
// Persisted documents hold both layouts; connected hardware exposes its supported target only.
const compactHardware = editor().vm
compactHardware.state.data.supportedProfiles = ['compact']
compactHardware.state.profile = 'compact'
compactHardware.selectProfile('large')
assert.equal(compactHardware.state.profile, 'compact')
const largeHardware = editor().vm
largeHardware.state.data.supportedProfiles = ['large']
largeHardware.selectProfile('compact')
assert.equal(largeHardware.state.profile, 'large')
// Layer movement changes only stacking, supports undo/reset, and rejects corrupt orders.
const layers = editor().vm
const positionsBefore = copy(layers.state.draft.layouts)
const initialLayers = layers.layerWidgets.map(widget => widget.id)
layers.reorderLayer(initialLayers.at(-1), initialLayers[0])
assert.equal(layers.layerWidgets[0].id, initialLayers.at(-1))
assert.equal(layers.renderWidgets.at(-1).id, initialLayers.at(-1))
assert.deepEqual(layers.state.draft.layouts, positionsBefore)
assert.equal(validDocument(layers.state.draft, layers.state.data.metadata), true)
const savedLayers = copy(layers.state.draft.widgetOrder)
layers.undo()
assert.equal(layers.state.draft.widgetOrder, undefined)
layers.redo()
assert.deepEqual(layers.state.draft.widgetOrder, savedLayers)
layers.selectProfile('compact')
layers.reorderLayer('max_speed', 'torque_bar')
assert.deepEqual(layers.state.draft.widgetOrder.large, savedLayers.large)
layers.resetLayout()
assert.deepEqual(layers.state.draft.widgetOrder, savedLayers)
for (const order of [[], ['unknown'], [...savedLayers.large, savedLayers.large[0]], savedLayers.large.map(() => savedLayers.large[0])]) {
const invalid = copy(layers.state.draft)
invalid.widgetOrder.large = order
assert.equal(validDocument(invalid, layers.state.data.metadata), false)
}
layers.selectProfile('large')
layers.resetLayout()
assert.equal(layers.state.draft.widgetOrder, undefined)
console.log('Widget layer order: movement, selection, profile isolation, validation, undo and reset passed')
// Display preferences travel beside the layout and join the draft.
const displayRows = [
{ key: "DeveloperSidebar", label: "Developer Sidebar", group: "Big layout", choices: ["Off", "On"], available: true, reason: "", requires: "" },
{ key: "ShowBrakeStatus", label: "Wheel brake and acceleration colors", group: "widget", choices: ["Off", "On"], available: true, reason: "", requires: "" },
{ key: "HideLeadMarker", label: "Lead Indicator", group: "Lead car", choices: ["Off", "On"], available: true, reason: "", requires: "" },
{ key: "LeadInfo", label: "Lead Info", group: "Lead car", choices: ["Off", "Distance", "Speed"], available: true, reason: "", requires: "HideLeadMarker" },
{ key: "SignalMetrics", label: "C4 amber signal border", group: "Alerts and borders", choices: ["Off", "On"], available: true, reason: "", requires: "" },
]
const withDisplayData = () => ({ ...snapshot(), display: { rows: copy(displayRows),
values: { DeveloperSidebar: "On", ShowBrakeStatus: "Off", HideLeadMarker: "Off", LeadInfo: "Off", SignalMetrics: "Off" },
defaults: { DeveloperSidebar: "Off", ShowBrakeStatus: "Off", HideLeadMarker: "Off", LeadInfo: "Off", SignalMetrics: "Off" } } })
const merged = withDisplay(withDisplayData())
assert.equal(validSnapshot(merged), true)
assert.deepEqual(merged.document.display, { ...merged.defaults.display, DeveloperSidebar: "On" })
assert.equal(previewDocument(snapshot().document).developerSidebar, false)
assert.equal(merged.display.length, 5)
assert.deepEqual(layoutDocument(merged.document), snapshot().document)
assert.equal(validDocument({ ...merged.document, display: { SignalMetrics: true } }, merged.metadata), false)
assert.equal(validDocument({ ...merged.document, showMaxSpeed: "no" }, merged.metadata), false)
const display = editor()
display.vm.state.data = merged; display.vm.state.draft = copy(merged.document)
assert.deepEqual(display.vm.displayGroups.map(group => [group.name, group.rows.map(row => row.key)]),
[["Big layout", ["DeveloperSidebar"]], ["Lead car", ["HideLeadMarker", "LeadInfo"]], ["Alerts and borders", ["SignalMetrics"]]])
display.vm.setDisplay("DeveloperSidebar", "Off")
assert.equal(display.vm.displaySettingRow(display.vm.displayRow("DeveloperSidebar")).value, "Off")
assert.equal(display.vm.dirty, true)
assert.equal(previewDocument(display.vm.state.draft).developerSidebar, false)
assert.equal(display.backdropCalls.at(-1)[1].developerSidebar, false, "the draft choice reaches the native backdrop")
assert.deepEqual(layoutDocument(display.vm.state.draft), snapshot().document, "preview changes never rewrite widget positions")
assert.equal(display.vm.roadBounds.width, 2100)
assert.equal(display.vm.previewX("steering_wheel"), 1888)
assert.equal(display.vm.previewX("current_speed"), 790)
assert.equal(display.vm.previewX("cruise_limits"), 88)
assert.ok(display.vm.previewSnapTargets("steering_wheel").some(target =>
target.axis === "x" && target.label === "Current speed" && target.line === "center" && target.value === 780))
const wheelPoint = x => ({ ...display.event(100 + x / 2, 150), pointerType: "mouse", altKey: true })
display.vm.startDrag("steering_wheel", wheelPoint(1900))
display.vm.moveDrag(wheelPoint(1920))
assert.equal(display.vm.layout.steering_wheel.x, 1608, "dragging saves the movement without adding the runtime anchor offset")
display.vm.endDrag(wheelPoint(1920))
display.vm.undo()
const wide = editor()
wide.vm.state.data = merged; wide.vm.state.draft = copy(merged.document)
wide.vm.setDisplay("DeveloperSidebar", "Off")
// With the sidebar hidden every widget reaches the whole screen; with it showing, only the road beside it.
wide.vm.changePosition("cruise_limits", 5000, 100)
assert.equal(wide.vm.layout.cruise_limits.x, 2130 - 480)
assert.equal(wide.vm.previewX("cruise_limits"), 2130 - 480)
wide.vm.changePosition("steering_wheel", -999, 75)
assert.equal(wide.vm.layout.steering_wheel.x, -270)
assert.equal(wide.vm.previewX("steering_wheel"), 30, "a right-anchored widget reaches the left edge")
assert.equal(validDocument(wide.vm.state.draft, wide.vm.state.data.metadata), true)
wide.vm.setDisplay("DeveloperSidebar", "On")
assert.equal(wide.vm.limitProfile.widgets.steering_wheel.bounds, undefined)
assert.equal(wide.vm.previewX("cruise_limits"), 1830 - 480, "a position from the wider screen is shown inside the road")
assert.equal(wide.vm.previewX("steering_wheel"), 30)
wide.vm.changePosition("cruise_limits", 5000, 100)
assert.equal(wide.vm.layout.cruise_limits.x, 1830 - 480)
assert.equal(display.vm.previewX("steering_wheel"), 1888)
display.vm.undo()
assert.equal(display.vm.displayValue("DeveloperSidebar"), "On")
assert.equal(display.vm.roadBounds.width, 1800)
assert.equal(display.vm.previewX("steering_wheel"), 1588)
assert.equal(previewDocument(display.vm.state.draft).developerSidebar, true)
for (const sidebar of ["Off", "On"]) {
const allWidgets = editor().vm
allWidgets.state.data = copy(merged); allWidgets.state.draft = copy(merged.document)
for (const id of ["pip_left", "pip_right"]) {
allWidgets.profile.widgets[id] = { ...widget(id, "pip_camera", 600, 600, 54, 426), layer: "underlay", colors: {},
expandable: true, bounds: { x: 30, y: 30, width: 2100, height: 1020 } }
allWidgets.layout[id] = { x: 54, y: 426, enabled: true }
}
allWidgets.setDisplay("DeveloperSidebar", sidebar)
for (const [id, widget] of Object.entries(allWidgets.profile.widgets)) {
const [width, height] = documentHelpers.widgetSize(widget, allWidgets.layout[id])
for (const [x, y] of [[-9999, -9999], [-9999, 9999], [9999, -9999], [9999, 9999]]) {
allWidgets.changePosition(id, x, y)
assert.equal(allWidgets.previewX(id), x < 0 ? 30 : (sidebar === "Off" ? 2130 : 1830) - width, id)
assert.equal(allWidgets.layout[id].y, y < 0 ? 30 : 1050 - height, id)
assert.equal(validDocument(allWidgets.state.draft, allWidgets.state.data.metadata), true, id)
}
}
}
const shiftedTray = editor(), shifted = shiftedTray.vm
shifted.state.data = { ...merged, supportedProfiles: ["large", "compact"] }; shifted.state.draft = copy(merged.document)
shifted.setDisplay("DeveloperSidebar", "Off")
shifted.layout.steering_wheel.enabled = false
const trayPoint = x => ({ ...shiftedTray.event(100 + x / 2, 200), pointerType: "mouse", altKey: true })
shifted.startDrag("steering_wheel", trayPoint(1900), true)
shifted.moveDrag(trayPoint(1940))
assert.equal(shifted.previewX("steering_wheel") + 96, 1940, "a tray drop stays centered under the pointer")
shifted.endDrag(trayPoint(1940))
shifted.selectProfile("compact")
assert.equal(shifted.expandedRoad, false)
assert.equal(shifted.roadBounds.width, 476)
const leadInfo = display.vm.displayRow("LeadInfo")
assert.equal(display.vm.displaySettingRow(leadInfo).reason, "Turn on Lead Indicator first")
display.vm.setDisplay("LeadInfo", "Distance")
assert.equal(display.vm.displayValue("LeadInfo"), "Off", "blocked until the draft shows the Lead indicator")
display.vm.setDisplay("HideLeadMarker", "On")
display.vm.setDisplay("LeadInfo", "Distance")
assert.equal(display.vm.displayValue("LeadInfo"), "Distance")
assert.equal(display.vm.dirty, true)
display.vm.undo(); display.vm.undo()
assert.equal(display.vm.dirty, false)
display.vm.state.selected = "cruise_limits"
display.vm.setShowMaxSpeed({ target: { checked: false } })
assert.equal(display.vm.state.draft.showMaxSpeed, false)
assert.equal(validDocument(display.vm.state.draft, display.vm.state.data.metadata), true)
display.vm.resetToStock()
assert.deepEqual(display.vm.state.draft, merged.defaults)
assert.match(OnroadLayoutPage.template, /state\.inspectorPanel = 'display'">Options<\/button>/)
assert.doesNotMatch(OnroadLayoutPage.template, /Follow Appearance Settings/)
assert.equal(OnroadLayoutPage.setup({ mode: "local", unauthorized() {}, initialPanel: "display" }).state.inspectorPanel, "display")
assert.equal(OnroadLayoutPage.setup({ mode: "local", unauthorized() {}, initialPanel: "display", projection: true }).state.inspectorPanel, "widgets")
// Target switches must not be swallowed while a layout loads or a drag flag lingers.
const switching = editor()
switching.vm.state.status = "loading"
let proceeded = 0
switching.vm.requestLeave(() => { proceeded++ })
assert.equal(proceeded, 1)
switching.vm.state.status = "ready"
switching.vm.state.layerDrag = { id: "current_speed", pointerId: 9, rows: [], before: copy(switching.vm.state.draft) }
switching.vm.requestLeave(() => { proceeded++ })
assert.equal(switching.vm.state.layerDrag, null, "a stray layer drag is cancelled")
assert.equal(proceeded, 2)
switching.vm.state.status = "saving"
switching.vm.requestLeave(() => { proceeded++ })
assert.equal(proceeded, 2, "an in-flight save finishes first")
assert.match(OnroadLayoutPage.template, /<section class="gx-settings gx-layout" aria-label="Colors and layout" :aria-busy/)
assert.match(OnroadLayoutPage.template, /<div class="gx-layout__workspace" :inert="busy">/)
// An explicit reload replaces a background poll instead of being dropped.
const reloading = feedFixture()
reloading.feed.start(); await reloading.reply(0, withDisplayData())
assert.equal(reloading.feed.data.document.display.SignalMetrics, "Off")
reloading.feed.load(true)
assert.equal(reloading.requests.length, 2)
reloading.feed.load(true)
assert.equal(reloading.requests.length, 2, "polls never stack")
reloading.feed.load()
assert.equal(reloading.requests.length, 3)
assert.equal(reloading.updates.at(-1).status, "loading")
await reloading.reply(2, withDisplayData())
assert.equal(reloading.updates.at(-1).notice, "Reloaded the saved layout.")
assert.ok(reloading.updates.at(-1).draft)
const settled = reloading.updates.length
await reloading.reply(1, withDisplayData())
assert.equal(reloading.updates.length, settled, "the replaced poll is ignored")
const failedSave = feedFixture()
failedSave.feed.start(); await failedSave.reply(0, withDisplayData())
failedSave.feed.save(failedSave.feed.data.document)
await failedSave.reply(1, { ...withDisplayData(), failed: ["Lead Info"] })
assert.match(failedSave.updates.at(-1).notice, /did not change: Lead Info/)
console.log('Display options, target switching and reload during polling passed')
// A closed developer sidebar makes the torque arc taller than its narrow-road box.
{
const sample = snapshot(), profile = sample.metadata.profiles.large
profile.widgets.cruise_limits.height = 168
profile.widgets.cruise_limits.confirmation = { x: 172, y: -12, width: 419, height: 204 }
profile.widgets.torque_bar.confirmationObstacle = { x: 0, y: -48, width: 1250, height: 289 }
assert.equal(documentHelpers.overlapsReserved(profile, 'cruise_limits', 88, 550, sample.document.layouts.large), true)
delete profile.widgets.torque_bar.confirmationObstacle
assert.equal(documentHelpers.overlapsReserved(profile, 'cruise_limits', 88, 550, sample.document.layouts.large), false)
profile.widgets.cruise_limits.height = 276
assert.equal(documentHelpers.overlapsReserved(profile, 'cruise_limits', 88, 500, sample.document.layouts.large), true)
}