148 lines
7.3 KiB
JavaScript
148 lines
7.3 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { runInNewContext } from 'node:vm'
|
|
import { parse } from '@vue/compiler-sfc'
|
|
import * as Vue from 'vue'
|
|
import ts from 'typescript'
|
|
|
|
// Run the real root controller with an in-memory Wails bridge, so manual
|
|
// device detection exercises selection, preview and wireless connection together.
|
|
const { descriptor } = parse(readFileSync(new URL('../src/App.vue', import.meta.url), 'utf8'))
|
|
const source = descriptor.scriptSetup.content + '\nexport { devices, activeId, live, refreshDevices, selectDevice, stopStream, onConnect, onUSBConnect, onDisconnect };'
|
|
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
|
|
const clone = value => JSON.parse(JSON.stringify(value))
|
|
const settle = async () => { for (let i = 0; i < 20; i++) await Promise.resolve() }
|
|
const usb = (state = 'device', id = 'usb-phone') => ({ id, model: 'USB phone', mode: 'usb', online: state === 'device', state, width: 1080, height: 2400 })
|
|
const lan = (id = '192.0.2.1:5555') => ({ ...usb(), id, model: 'Wireless phone', mode: 'lan' })
|
|
|
|
function harness(initial = []) {
|
|
const calls = [], logs = [], timers = new Map(), cleanup = [], mounted = []
|
|
let available = initial, selected = '', timerID = 0, beforeList
|
|
let prefs = { theme: 'system', lastTab: 'workspace', splitRatio: .55, screenOff: false }
|
|
const api = {
|
|
async ListDevices() { calls.push(['list']); if (beforeList) await beforeList(); return clone(available) },
|
|
async SelectDevice(id) { calls.push(['select', id]); selected = id; available = available.map(d => d.id === id && d.state === 'disconnected' ? usb('device', id) : d) },
|
|
async StartPreviewStream(id) { calls.push(['preview', id]); return { ok: true, message: 'http://localhost:1234/mjpeg' } },
|
|
async StopPreviewStream() { calls.push(['stop']) },
|
|
async DisconnectDevice(id) { calls.push(['disconnect', id]); available = available.flatMap(d => d.id !== id ? [d] : d.mode === 'usb' ? [{ ...d, online: false, state: 'disconnected' }] : []); return { ok: true } },
|
|
async ConnectWireless({ hostPort }) { calls.push(['connect', hostPort]); if (!available.some(d => d.id === hostPort)) available.push(lan(hostPort)); selected = hostPort; return { ok: true, message: 'connected' } },
|
|
async GetActiveDevice() { return clone(available.find(d => d.id === selected) || {}) },
|
|
}
|
|
const module = { exports: {} }
|
|
runInNewContext(compiled, {
|
|
module, exports: module.exports,
|
|
require(name) {
|
|
if (name === 'vue') return { ...Vue, onMounted: fn => mounted.push(fn), onUnmounted: fn => cleanup.push(fn) }
|
|
if (name.endsWith('/useLogs')) return { useLogs: () => ({ lines: Vue.ref([]), log: line => logs.push(line), load: async () => {} }) }
|
|
if (name.endsWith('/useConversations')) return { useConversations: () => ({ messages: Vue.ref([]), sending: Vue.ref(false), load: async () => {} }) }
|
|
if (name.endsWith('/prefs')) return { loadPrefs: () => prefs, savePrefs: patch => (prefs = { ...prefs, ...patch }) }
|
|
if (name.endsWith('/touch')) return { TouchQueue: class { push() {} } }
|
|
if (name.endsWith('/device')) return { Service: api }
|
|
if (name.endsWith('/agent')) return { Service: { ProtocolVersion: async () => 'test', Status: async () => ({}) } }
|
|
return {}
|
|
},
|
|
window: { matchMedia: () => ({ matches: true, addEventListener() {}, removeEventListener() {} }), addEventListener() {}, removeEventListener() {} },
|
|
document: { documentElement: { dataset: {} } },
|
|
crypto: globalThis.crypto,
|
|
setTimeout: fn => { const id = ++timerID; timers.set(id, fn); return id },
|
|
clearTimeout: id => timers.delete(id),
|
|
})
|
|
return {
|
|
app: module.exports, calls, logs, timers,
|
|
devices: value => { available = value },
|
|
beforeList: fn => { beforeList = fn },
|
|
async mount() { for (const fn of mounted) await fn() },
|
|
async dispose() { for (const fn of cleanup) fn(); await settle() },
|
|
}
|
|
}
|
|
|
|
test('manual refresh detects USB authorization and unplugging', async () => {
|
|
const h = harness([usb('unauthorized')])
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.activeId.value, 'usb-phone')
|
|
assert.equal(h.app.live.value, false)
|
|
h.devices([usb()])
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.live.value, true)
|
|
assert.deepEqual(h.calls.filter(c => c[0] === 'preview'), [['preview', 'usb-phone']])
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.calls.filter(c => c[0] === 'preview').length, 1)
|
|
h.devices([])
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.live.value, false)
|
|
assert.equal(h.app.activeId.value, '')
|
|
assert.equal(h.calls.some(c => c[0] === 'connect' || c[0] === 'disconnect'), false)
|
|
await h.dispose()
|
|
})
|
|
|
|
test('a deliberate preview stop remains stopped on subsequent USB refreshes', async () => {
|
|
const h = harness([usb()])
|
|
await h.app.refreshDevices()
|
|
await h.app.stopStream()
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.live.value, false)
|
|
assert.equal(h.calls.filter(c => c[0] === 'preview').length, 1)
|
|
await h.app.onDisconnect('usb-phone')
|
|
assert.equal(h.calls.some(c => c[0] === 'disconnect'), true)
|
|
assert.equal(h.app.live.value, false)
|
|
assert.equal(h.app.devices.value[0].state, 'disconnected')
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.live.value, false)
|
|
await h.app.onUSBConnect('usb-phone')
|
|
assert.equal(h.app.live.value, true)
|
|
await h.dispose()
|
|
})
|
|
|
|
test('manual refresh preserves an online wireless selection; explicit USB choice switches preview', async () => {
|
|
const h = harness([lan()])
|
|
await h.app.refreshDevices()
|
|
h.devices([lan(), usb()])
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.app.activeId.value, '192.0.2.1:5555')
|
|
assert.equal(h.calls.some(c => c[0] === 'preview'), false)
|
|
await h.app.onUSBConnect('usb-phone')
|
|
assert.equal(h.app.activeId.value, 'usb-phone')
|
|
assert.deepEqual(h.calls.filter(c => c[0] === 'preview'), [['preview', 'usb-phone']])
|
|
assert.equal(h.calls.some(c => c[0] === 'connect'), false)
|
|
await h.dispose()
|
|
})
|
|
|
|
test('wireless connect previews its requested device even when USB is already attached', async () => {
|
|
const h = harness([usb()])
|
|
await h.app.onConnect('192.0.2.8:5555')
|
|
assert.equal(h.app.activeId.value, '192.0.2.8:5555')
|
|
assert.ok(h.calls.filter(c => c[0] === 'preview').every(c => c[1] === '192.0.2.8:5555'))
|
|
assert.equal(h.app.live.value, true)
|
|
await h.dispose()
|
|
})
|
|
|
|
test('overlapping manual refreshes share the same device scan', async () => {
|
|
const h = harness([usb('unauthorized')])
|
|
let release
|
|
h.beforeList(() => new Promise(resolve => { release = resolve }))
|
|
const first = h.app.refreshDevices()
|
|
const second = h.app.refreshDevices()
|
|
assert.equal(first, second)
|
|
assert.equal(h.calls.filter(c => c[0] === 'list').length, 1)
|
|
h.beforeList(undefined)
|
|
release()
|
|
await first
|
|
assert.equal(h.timers.size, 0)
|
|
await h.dispose()
|
|
})
|
|
|
|
|
|
test('startup and manual refresh do not register automatic device scans', async () => {
|
|
const h = harness([usb('unauthorized')])
|
|
await h.mount()
|
|
assert.equal(h.calls.filter(c => c[0] === 'list').length, 1)
|
|
assert.equal(h.timers.size, 0)
|
|
await settle()
|
|
assert.equal(h.calls.filter(c => c[0] === 'list').length, 1)
|
|
await h.app.refreshDevices()
|
|
assert.equal(h.calls.filter(c => c[0] === 'list').length, 2)
|
|
assert.equal(h.timers.size, 0)
|
|
await h.dispose()
|
|
})
|