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() })