Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
7be0145b4This file is pinned to the selected release. Download the original to open it in your design tool.
import assert from 'node:assert/strict'
import { makeXsArchive, modDefinition } from '../../firmware/contracts/testing/xsa-fixture.js'
import { describe, it } from 'node:test'
import {
clientPointFromTouch,
createHostAudioInBridge,
createHostButtonBridge,
createHostCameraBridge,
createHostDriverBridge,
installModArchiveIntoWasm,
summarizeImageData,
} from './bridge.mjs'
describe('Host.Button bridge', () => {
it('emits normalized press and release edges for button-aware MOD compatibility', () => {
const scheduled = []
const events = []
const bridge = createHostButtonBridge({
logger: (message) => events.push(message),
setTimeoutFn: (callback, delay) => {
scheduled.push({ callback, delay })
return scheduled.length
},
})
const firmwareButton = new bridge.Button.a({ onPush: () => events.push('firmware:a') })
assert.equal(bridge.Button.read('a'), 0)
assert.equal(firmwareButton.read(), 0)
assert.equal(bridge.read('a'), 0)
bridge.push('a')
assert.equal(firmwareButton.read(), 1)
assert.equal(bridge.Button.read('a'), 1)
assert.equal(bridge.read('a'), 1)
assert.deepEqual(events, ['[bridge] Host.Button.a pushed', 'firmware:a'])
assert.equal(scheduled[0].delay, 120)
scheduled[0].callback()
assert.equal(firmwareButton.read(), 0)
assert.equal(bridge.read('a'), 0)
assert.deepEqual(events, ['[bridge] Host.Button.a pushed', 'firmware:a', 'firmware:a'])
})
it('ignores unknown button names without throwing', () => {
const bridge = createHostButtonBridge({ logger: () => {} })
assert.doesNotThrow(() => bridge.push('x'))
assert.equal(bridge.read('x'), undefined)
})
it('keeps a repeated push pressed until the latest release timeout', () => {
const scheduled = []
const edges = []
const bridge = createHostButtonBridge({
logger: () => {},
setTimeoutFn: (callback) => scheduled.push(callback),
})
new bridge.Button.a({ onPush: () => edges.push(bridge.read('a')) })
bridge.push('a')
bridge.push('a')
scheduled[0]()
assert.equal(bridge.read('a'), 1)
assert.deepEqual(edges, [1, 1])
scheduled[1]()
assert.equal(bridge.read('a'), 0)
assert.deepEqual(edges, [1, 1, 0])
})
})
describe('WASM screen diagnostics', () => {
it('summarizes sampled image data so blank alpha buffers are visible in diagnostics', () => {
const stats = summarizeImageData(new Uint8ClampedArray([0, 0, 0, 0, 12, 0, 0, 255]))
assert.equal(stats.samples, 2)
assert.equal(stats.nonZeroAlpha, 1)
assert.equal(stats.nonZeroRgb, 1)
assert.deepEqual(stats.firstPixel, [0, 0, 0, 0])
})
})
describe('Host.Driver bridge', () => {
it('records firmware rotation messages and notifies the simulator scene', () => {
const events = []
const bridge = createHostDriverBridge({
onRotation: (rotation, time) => events.push({ rotation, time }),
onTorque: (torque) => events.push({ torque }),
})
bridge.applyRotation({ rotation: { y: 0.2, p: -0.1, r: 0.03 }, time: 0.5 })
bridge.setTorque(false)
assert.deepEqual(bridge.getRotation(), { y: 0.2, p: -0.1, r: 0.03 })
assert.deepEqual(events, [{ rotation: { y: 0.2, p: -0.1, r: 0.03 }, time: 0.5 }, { torque: false }])
})
})
describe('Host.Audio bridge', () => {
it('records WebM/Opus microphone audio when the browser supports it', async () => {
const stopped = []
const recorderOptions = []
const webmHeader = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])
class FakeMediaRecorder {
static isTypeSupported(type) {
return type === 'audio/webm;codecs=opus'
}
constructor(stream, options) {
this.stream = stream
recorderOptions.push(options)
}
start() {
this.ondataavailable?.({ data: webmHeader.buffer })
}
stop() {
this.onstop?.()
}
}
const bridge = createHostAudioInBridge({
mediaDevices: {
async getUserMedia(request) {
assert.deepEqual(request, { audio: true })
return { getTracks: () => [{ stop: () => stopped.push('track') }] }
},
},
MediaRecorder: FakeMediaRecorder,
})
const buffer = await bridge.record(1)
assert.deepEqual(Array.from(new Uint8Array(buffer)), Array.from(webmHeader))
assert.equal(buffer.mimeType, 'audio/webm;codecs=opus')
assert.equal(buffer.filename, 'speak.webm')
assert.deepEqual(recorderOptions, [{ mimeType: 'audio/webm;codecs=opus' }])
assert.deepEqual(stopped, ['track'])
})
it('records WAV-compatible microphone audio when WAV is the supported browser format', async () => {
const stopped = []
const recorderOptions = []
const wavHeader = new TextEncoder().encode('RIFFxxxxWAVE')
class FakeMediaRecorder {
static isTypeSupported(type) {
return type === 'audio/wav'
}
constructor(stream, options) {
this.stream = stream
recorderOptions.push(options)
}
start() {
this.ondataavailable?.({ data: wavHeader.buffer })
}
stop() {
this.onstop?.()
}
}
const bridge = createHostAudioInBridge({
mediaDevices: {
async getUserMedia(request) {
assert.deepEqual(request, { audio: true })
return { getTracks: () => [{ stop: () => stopped.push('track') }] }
},
},
MediaRecorder: FakeMediaRecorder,
})
const buffer = await bridge.record(1)
assert.equal(new TextDecoder().decode(buffer.slice(0, 4)), 'RIFF')
assert.equal(new TextDecoder().decode(buffer.slice(8, 12)), 'WAVE')
assert.equal(buffer.mimeType, 'audio/wav')
assert.equal(buffer.filename, 'speak.wav')
assert.deepEqual(recorderOptions, [{ mimeType: 'audio/wav' }])
assert.deepEqual(stopped, ['track'])
})
it('rejects microphone recording before requesting permission when no format is supported', async () => {
let requested = false
class FakeMediaRecorder {
static isTypeSupported() {
return false
}
}
const bridge = createHostAudioInBridge({
mediaDevices: {
async getUserMedia() {
requested = true
throw new Error('should not request microphone without WAV support')
},
},
MediaRecorder: FakeMediaRecorder,
})
await assert.rejects(bridge.record(100), { code: 'UNSUPPORTED' })
assert.equal(requested, false)
})
})
describe('Host.Camera bridge', () => {
it('returns deterministic RGB565LE frames only after explicitly selecting simulation', async () => {
const bridge = createHostCameraBridge()
assert.equal(bridge.availability(), 'unavailable')
assert.throws(() => bridge.capture(), { code: 'CLOSED' })
await bridge.start({ useBrowserCamera: false })
assert.equal(bridge.availability(), 'simulated')
const first = bridge.capture({ width: 4, height: 3, imageType: 'rgb565le' })
const second = bridge.capture({ width: 4, height: 3, imageType: 'rgb565le' })
assert.ok(first)
assert.ok(second)
assert.equal(first.source, 'simulated')
assert.equal(first.width, 4)
assert.equal(first.height, 3)
assert.equal(first.imageType, 'rgb565le')
assert.equal(first.buffer.byteLength, 4 * 3 * 2)
assert.deepEqual(new Uint8Array(first.buffer), new Uint8Array(second.buffer))
})
it('tracks start and stop without requiring browser media devices', async () => {
const bridge = createHostCameraBridge()
await bridge.start({ width: 2, height: 2, useBrowserCamera: false })
assert.equal(bridge.isStarted(), true)
bridge.stop()
bridge.stop()
assert.equal(bridge.isStarted(), false)
})
it('captures ready browser video frames as RGB565LE through canvas', async () => {
const calls = []
const stream = { getTracks: () => [{ stop: () => calls.push('stop') }] }
const video = {
readyState: 2,
videoWidth: 16,
videoHeight: 16,
play: async () => calls.push('play'),
}
const canvas = {
getContext: () => ({
drawImage: (...args) => calls.push(['drawImage', ...args.slice(1)]),
getImageData: () => ({
data: new Uint8ClampedArray([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]),
}),
}),
}
const bridge = createHostCameraBridge({
canvasElement: canvas,
navigatorObj: {
mediaDevices: {
async getUserMedia(constraints) {
calls.push(['getUserMedia', constraints])
return stream
},
},
},
videoElement: video,
})
await bridge.start({ useBrowserCamera: true })
const frame = bridge.capture({ width: 2, height: 2, imageType: 'rgb565le' })
assert.equal(bridge.isStarted(), true)
assert.equal(bridge.isBrowserCameraStarted(), true)
assert.deepEqual(calls.slice(0, 3), [['getUserMedia', { video: true }], 'play', ['drawImage', 0, 0, 2, 2]])
assert.equal(video.srcObject, stream)
assert.equal(canvas.width, 2)
assert.equal(canvas.height, 2)
assert.equal(frame.width, 2)
assert.equal(frame.height, 2)
assert.equal(frame.imageType, 'rgb565le')
assert.equal(frame.buffer.byteLength, 2 * 2 * 2)
assert.deepEqual(new Uint8Array(frame.buffer), new Uint8Array([0x00, 0xf8, 0xe0, 0x07, 0x1f, 0x00, 0xff, 0xff]))
bridge.stop()
assert.equal(video.srcObject, null)
assert.equal(calls.at(-1), 'stop')
})
it('reports unavailable media APIs without generating a successful image', async () => {
const bridge = createHostCameraBridge({ navigatorObj: {}, documentObj: undefined })
await assert.rejects(bridge.start(), { code: 'UNSUPPORTED' })
assert.equal(bridge.isStarted(), false)
assert.throws(() => bridge.capture(), { code: 'CLOSED' })
})
it('returns permission failure and permits a later explicit retry', async () => {
let denied = true
const bridge = createHostCameraBridge({
navigatorObj: {
mediaDevices: {
async getUserMedia() {
if (denied) throw new Error('denied')
return { getTracks: () => [] }
},
},
},
videoElement: { play: async () => {} },
})
await assert.rejects(bridge.start(), /denied/)
assert.equal(bridge.isStarted(), false)
denied = false
await bridge.start()
assert.equal(bridge.isBrowserCameraStarted(), true)
bridge.stop()
})
it('returns no frame while video is warming up and surfaces canvas failures', async () => {
const video = { readyState: 1, videoWidth: 0, videoHeight: 0 }
const bridge = createHostCameraBridge({
canvasElement: {
getContext: () => {
throw new Error('canvas failed')
},
},
navigatorObj: {
mediaDevices: {
async getUserMedia() {
return { getTracks: () => [] }
},
},
},
videoElement: video,
})
await bridge.start()
assert.equal(bridge.capture(), undefined)
Object.assign(video, { readyState: 2, videoWidth: 16, videoHeight: 16 })
assert.throws(() => bridge.capture(), /canvas failed/)
bridge.stop()
})
it('keeps an already-started browser camera stream when firmware starts camera preview', async () => {
const calls = []
const stream = { getTracks: () => [{ stop: () => calls.push('stop') }] }
const video = {
readyState: 2,
videoWidth: 16,
videoHeight: 16,
play: async () => calls.push('play'),
}
const bridge = createHostCameraBridge({
navigatorObj: {
mediaDevices: {
async getUserMedia(constraints) {
calls.push(['getUserMedia', constraints])
return stream
},
},
},
videoElement: video,
})
await bridge.start({ useBrowserCamera: true })
await bridge.start({ width: 200, height: 120, imageType: 'rgb565le' })
assert.equal(bridge.isBrowserCameraStarted(), true)
assert.equal(video.srcObject, stream)
assert.deepEqual(calls, [['getUserMedia', { video: true }], 'play'])
})
it('starts a fresh browser stream for a second firmware camera preview', async () => {
const calls = []
const streams = [0, 1].map((index) => ({
getTracks: () => [{ stop: () => calls.push(['stop', index]) }],
}))
const secondStream = streams[1]
const video = {
readyState: 2,
videoWidth: 16,
videoHeight: 16,
play: async () => calls.push('play'),
}
const bridge = createHostCameraBridge({
navigatorObj: {
mediaDevices: {
async getUserMedia() {
const stream = streams.shift()
calls.push('getUserMedia')
return stream
},
},
},
videoElement: video,
})
await bridge.start({ useBrowserCamera: true })
bridge.stop()
await bridge.start({ useBrowserCamera: true })
assert.equal(bridge.isBrowserCameraStarted(), true)
assert.equal(video.srcObject, secondStream)
assert.deepEqual(calls, ['getUserMedia', 'play', ['stop', 0], 'getUserMedia', 'play'])
})
it('allows an explicit non-browser camera start to stop an active browser stream', async () => {
const calls = []
const stream = { getTracks: () => [{ stop: () => calls.push('stop') }] }
const video = { readyState: 2, videoWidth: 16, videoHeight: 16, play: async () => calls.push('play') }
const bridge = createHostCameraBridge({
navigatorObj: {
mediaDevices: {
async getUserMedia() {
return stream
},
},
},
videoElement: video,
})
await bridge.start({ useBrowserCamera: true })
await bridge.start({ useBrowserCamera: false })
assert.equal(bridge.isBrowserCameraStarted(), false)
assert.equal(video.srcObject, null)
assert.deepEqual(calls, ['play', 'stop'])
})
it('stops late-resolving browser streams when camera was stopped during permission prompt', async () => {
let resolveStream
const stopped = []
const stream = { getTracks: () => [{ stop: () => stopped.push('stop') }] }
const video = { srcObject: undefined, play: async () => {} }
const bridge = createHostCameraBridge({
navigatorObj: {
mediaDevices: {
getUserMedia() {
return new Promise((resolve) => {
resolveStream = resolve
})
},
},
},
videoElement: video,
})
const startPromise = bridge.start({ useBrowserCamera: true })
const rejected = assert.rejects(startPromise, { code: 'CANCELLED' })
await Promise.resolve()
bridge.stop()
resolveStream(stream)
await rejected
assert.equal(bridge.isStarted(), false)
assert.equal(bridge.isBrowserCameraStarted(), false)
assert.equal(video.srcObject, null)
assert.deepEqual(stopped, ['stop'])
})
it('rejects unsupported formats and invalid dimensions before allocation', async () => {
const bridge = createHostCameraBridge()
await bridge.start({ useBrowserCamera: false })
assert.throws(() => bridge.capture({ imageType: 'jpeg' }), { code: 'UNSUPPORTED' })
for (const width of [0, -1, 1.5, NaN, Infinity, 321])
assert.throws(() => bridge.capture({ width }), { code: 'INVALID_ARGUMENT' })
assert.equal('captureJpeg' in bridge, false)
})
it('stops every track even when detaching video or a track fails, and rejects reuse', async () => {
const stops = []
let rejectDetach = false
const video = {
play: async () => {},
set srcObject(value) {
if (rejectDetach && value === null) throw new Error('detach failed')
},
}
const bridge = createHostCameraBridge({
videoElement: video,
navigatorObj: {
mediaDevices: {
async getUserMedia() {
return {
getTracks: () => [
{
stop() {
stops.push(1)
throw new Error('track failed')
},
},
{
stop() {
stops.push(2)
},
},
],
}
},
},
},
})
await bridge.start()
rejectDetach = true
assert.throws(() => bridge.stop(), /detach failed/)
assert.deepEqual(stops, [1, 2])
await assert.rejects(bridge.start(), /detach failed/)
})
it('a late video.play completion cannot revive or stop a newer stream', async () => {
let finishPlay
const stopped = []
let count = 0
const video = {
play: () =>
count === 1
? new Promise((resolve) => {
finishPlay = resolve
})
: Promise.resolve(),
}
const bridge = createHostCameraBridge({
videoElement: video,
navigatorObj: {
mediaDevices: {
async getUserMedia() {
const id = ++count
return { id, getTracks: () => [{ stop: () => stopped.push(id) }] }
},
},
},
})
const old = bridge.start()
const rejected = assert.rejects(old, { code: 'CANCELLED' })
await new Promise((resolve) => setImmediate(resolve))
bridge.stop()
await bridge.start()
finishPlay()
await rejected
assert.equal(video.srcObject.id, 2)
assert.deepEqual(stopped, [1])
bridge.stop()
assert.deepEqual(stopped, [1, 2])
})
it('100 starts and stops return all acquired media tracks', async () => {
let acquired = 0,
released = 0
const video = { play: async () => {} }
const bridge = createHostCameraBridge({
videoElement: video,
navigatorObj: {
mediaDevices: {
async getUserMedia() {
acquired++
return { getTracks: () => [{ stop: () => released++ }] }
},
},
},
})
for (let index = 0; index < 100; index++) {
await bridge.start()
bridge.stop()
bridge.stop()
assert.equal(acquired, released)
assert.equal(video.srcObject, null)
assert.equal(bridge.isStarted(), false)
}
})
})
describe('touch coordinate bridge', () => {
it('uses viewport-relative client coordinates for hidden-canvas touch forwarding', () => {
const point = clientPointFromTouch({ clientX: 42, clientY: 24, pageX: 1042, pageY: 2024 })
assert.deepEqual(point, { x: 42, y: 24 })
})
})
describe('MOD archive bridge', () => {
it('rejects incompatible metadata before calling any WASM allocator or install hook', () => {
let allocated = false
for (const metadata of [null, { ...modDefinition, hostApiVersion: 999 }]) {
assert.throws(() =>
installModArchiveIntoWasm(
{
_malloc() {
allocated = true
return 8
},
HEAPU8: new Uint8Array(4096),
},
{ name: 'bad.xsa', bytes: makeXsArchive({ metadata }) }
)
)
assert.equal(allocated, false)
}
})
it('reports empty when no archive is installed', () => {
assert.deepEqual(installModArchiveIntoWasm({}, null), { status: 'empty' })
})
it('copies archive bytes into wasm memory, calls the install hook, and frees memory', () => {
const bytes = makeXsArchive()
const heap = new Uint8Array(bytes.length + 32)
const calls = []
const wasmModule = {
HEAPU8: heap,
_malloc(size) {
calls.push(['malloc', size])
return 8
},
_free(pointer) {
calls.push(['free', pointer])
},
_wasmModInstallArchive(pointer, size) {
calls.push(['hook', pointer, size, Array.from(heap.slice(pointer, pointer + size))])
return 0
},
}
const result = installModArchiveIntoWasm(wasmModule, {
name: 'mod.xsa',
bytes,
size: 3,
})
assert.deepEqual(result, {
status: 'installed',
hook: '_wasmModInstallArchive',
name: 'mod.xsa',
size: bytes.length,
result: 0,
})
assert.deepEqual(calls, [
['malloc', bytes.length],
['hook', 8, bytes.length, Array.from(bytes)],
['free', 8],
])
})
it('prepares archive bytes as a launch archive when no explicit install hook exists', () => {
const bytes = makeXsArchive()
const heap = new Uint8Array(bytes.length + 16)
const calls = []
const result = installModArchiveIntoWasm(
{
HEAPU8: heap,
_malloc(size) {
calls.push(['malloc', size])
return 4
},
_free(pointer) {
calls.push(['free', pointer])
},
},
{ name: 'mod.xsa', bytes }
)
assert.deepEqual(result, { status: 'prepared', pointer: 4, name: 'mod.xsa', size: bytes.length })
assert.deepEqual(heap.slice(4, 4 + bytes.length), bytes)
assert.deepEqual(calls, [['malloc', bytes.length]])
})
})