mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-15 00:43:46 +08:00
9298db235b
Register Galaxy managed services, recording media and settings endpoints after their underlying owners exist. Cover authentication and cross-feature request behavior at the HTTP boundary.
137 lines
7.5 KiB
JavaScript
137 lines
7.5 KiB
JavaScript
import assert from "node:assert/strict"
|
|
import { BluetoothPage } from "../web/js/bluetooth.js"
|
|
import { ControllersFeed, ControllersPage, validControllersStatus } from "../web/js/controllers.js"
|
|
|
|
const status = () => ({ version: 1, available: true, editable: true, enabled: false, valid: true,
|
|
revision: "a".repeat(64), devices: [{ id: "hardware-1", name: "Steering remote", bus: 3 }],
|
|
slots: Array.from({ length: 13 }, (_, index) => ({ index, label: index < 3 ? `Quick Select ${index + 1}` : `Button ${index - 2}`,
|
|
key: null, available: index < 3 })),
|
|
options: [{ key: "brightness", label: "Brightness", section: "Display" }], bindings: [],
|
|
learning: null, testing: false, lastPress: null })
|
|
assert.equal(validControllersStatus(status()), true)
|
|
assert.equal(validControllersStatus({ ...status(), devices: [{ id: "bad", name: "Remote", bus: 9 }] }), false)
|
|
assert.equal(validControllersStatus({ ...status(), slots: status().slots.slice(1) }), false)
|
|
assert.equal(validControllersStatus({ ...status(), options: [{ key: "x\n", label: "Bad", section: "Display" }] }), false)
|
|
assert.equal(validControllersStatus({ ...status(), lastPress: { deviceId: "hardware-1", code: 65551,
|
|
slot: null, executed: false, message: "Test press detected" } }), true)
|
|
assert.match(BluetoothPage.template, /<ControllersPage :mode="mode" :unauthorized="unauthorized"/)
|
|
assert.match(ControllersPage.template, /Test Buttons for 20 Seconds/)
|
|
assert.match(ControllersPage.template, /Press a button for/)
|
|
assert.match(ControllersPage.template, /class="gx-switch"/)
|
|
assert.match(ControllersPage.template, /class="gx-switch__track"/)
|
|
assert.match(ControllersPage.template, /slot\.key \? 'Available when its control is ready' : 'Not assigned'/)
|
|
assert.match(ControllersPage.template, /gx-controllers__selector/)
|
|
|
|
const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve() }
|
|
function fixture() {
|
|
const requests = [], updates = [], timers = new Map(); let next = 0, unauthorized = 0
|
|
const feed = new ControllersFeed({ publish: (value) => updates.push(value), unauthorized: () => unauthorized++,
|
|
fetcher: (url, options) => new Promise((resolve) => requests.push({ url, options, resolve })),
|
|
later: (fn, ms) => { timers.set(++next, { fn, ms }); return next }, cancelTimer: (id) => timers.delete(id) })
|
|
async function reply(index, data = status(), code = 200) {
|
|
requests[index].resolve({ ok: code === 200, status: code, json: async () => data }); await flush()
|
|
}
|
|
function fire(ms) {
|
|
const item = [...timers.entries()].find(([, value]) => value.ms === ms)
|
|
assert.ok(item)
|
|
timers.delete(item[0]); item[1].fn()
|
|
}
|
|
return { feed, requests, updates, timers, reply, fire, get unauthorized() { return unauthorized } }
|
|
}
|
|
|
|
const normal = fixture()
|
|
normal.feed.start(); await normal.reply(0)
|
|
assert.equal(normal.requests[0].url, "./api/controllers/status")
|
|
assert.equal(normal.requests[0].options.credentials, "same-origin")
|
|
normal.fire(2000); await normal.reply(1)
|
|
const saved = normal.feed.action({ operation: "save", revision: "a".repeat(64), enabled: true,
|
|
slots: ["brightness", null, null, null, null, null, null, null, null, null] })
|
|
assert.equal(normal.requests[2].url, "./api/controllers/action")
|
|
assert.equal(normal.requests[2].options.method, "POST")
|
|
assert.deepEqual(JSON.parse(normal.requests[2].options.body), { operation: "save", revision: "a".repeat(64), enabled: true,
|
|
slots: ["brightness", null, null, null, null, null, null, null, null, null] })
|
|
await normal.reply(2); await saved
|
|
|
|
const learning = fixture()
|
|
learning.feed.start(); await learning.reply(0, { ...status(), learning: { slot: 4, expiresIn: 20 } })
|
|
learning.feed.stop()
|
|
assert.equal(learning.requests[1].url, "./api/controllers/action")
|
|
assert.deepEqual(JSON.parse(learning.requests[1].options.body), { operation: "cancel" })
|
|
assert.equal(learning.requests[1].options.keepalive, true)
|
|
assert.equal(learning.updates.at(-1).status, null)
|
|
|
|
const testMode = fixture()
|
|
testMode.feed.start(); await testMode.reply(0, { ...status(), testing: true })
|
|
testMode.feed.stop()
|
|
assert.deepEqual(JSON.parse(testMode.requests[1].options.body), { operation: "cancel" })
|
|
|
|
const stale = fixture()
|
|
stale.feed.start(); stale.feed.stop(); await stale.reply(0)
|
|
assert.equal(stale.updates.at(-1).status, null)
|
|
const denied = fixture()
|
|
denied.feed.start(); await denied.reply(0, {}, 401)
|
|
assert.equal(denied.unauthorized, 1)
|
|
const parked = fixture()
|
|
parked.feed.start(); await parked.reply(0, status())
|
|
parked.feed.action({ operation: "learn", revision: "a".repeat(64), slot: 1 }); await parked.reply(1, {}, 403)
|
|
assert.match(parked.updates.at(-1).error, /Turn off the vehicle/)
|
|
|
|
const component = ControllersPage.setup({ unauthorized() {} })
|
|
component.feed.publish({ status: status(), busy: false, error: "" })
|
|
assert.deepEqual(component.state.draft.slots, Array(10).fill(null))
|
|
component.state.draft.slots[0] = "brightness"
|
|
component.feed.publish({ status: { ...status(), revision: "b".repeat(64) }, busy: false, error: "" })
|
|
assert.equal(component.state.draft.revision, "a".repeat(64)) // A polling update cannot replace unsaved edits.
|
|
const page = { ...component, get canEdit() { return ControllersPage.computed.canEdit.call(this) },
|
|
get dirty() { return ControllersPage.computed.dirty.call(this) },
|
|
get changed() { return ControllersPage.computed.changed.call(this) } }
|
|
let calls = 0
|
|
component.feed.action = async () => { calls++; return status() }
|
|
ControllersPage.methods.learn.call(page, 4)
|
|
ControllersPage.methods.remove.call(page, { deviceId: "hardware-1", code: 1 })
|
|
assert.equal(calls, 0)
|
|
assert.match(ControllersPage.template, /Save these changes before learning or removing buttons/)
|
|
|
|
let finishReload
|
|
component.feed.refresh = () => new Promise((resolve) => { finishReload = resolve })
|
|
const reloading = ControllersPage.methods.reload.call(page)
|
|
assert.equal(component.state.draft.slots[0], "brightness") // The template still has a draft while reloading.
|
|
const afterReload = { ...status(), revision: "c".repeat(64) }
|
|
component.feed.publish({ status: afterReload, busy: false, error: "" })
|
|
finishReload(afterReload)
|
|
await reloading
|
|
assert.equal(component.state.draft.revision, "c".repeat(64))
|
|
assert.equal(component.state.draft.slots[0], null)
|
|
|
|
component.state.draft.enabled = true
|
|
const afterSave = { ...status(), revision: "d".repeat(64), enabled: true }
|
|
component.feed.action = async (payload) => {
|
|
assert.equal(payload.operation, "save")
|
|
component.feed.publish({ status: afterSave, busy: false, error: "" })
|
|
return afterSave
|
|
}
|
|
await ControllersPage.methods.save.call(page)
|
|
assert.equal(page.changed, false)
|
|
assert.equal(page.dirty, false)
|
|
assert.equal(component.state.draft.revision, afterSave.revision)
|
|
component.feed.stop()
|
|
assert.equal(component.state.draft, null)
|
|
|
|
const earlierDocument = globalThis.document
|
|
const listeners = new Map(), lifecycle = []
|
|
globalThis.document = { hidden: false, addEventListener(name, fn) { listeners.set(name, fn) },
|
|
removeEventListener(name) { listeners.delete(name) } }
|
|
const mounted = { mode: "local", feed: { start() { lifecycle.push("start") }, stop() { lifecycle.push("stop") } } }
|
|
ControllersPage.mounted.call(mounted)
|
|
assert.deepEqual(lifecycle, ["start"])
|
|
globalThis.document.hidden = true
|
|
listeners.get("visibilitychange")()
|
|
mounted.mode = "remote"
|
|
globalThis.document.hidden = false
|
|
ControllersPage.watch.mode.call(mounted)
|
|
ControllersPage.beforeUnmount.call(mounted)
|
|
assert.deepEqual(lifecycle, ["start", "stop", "stop", "stop"])
|
|
assert.equal(listeners.size, 0)
|
|
globalThis.document = earlierDocument
|
|
console.log("Controller buttons: source status validation, draft preservation, polling, auth, parked denial and capture cleanup passed")
|