import test from 'brittle'
import EventEmitter from 'bare-events'
import { Rig } from '../lib/rig.js'
import { FakeUnit, waitFor, sleep } from './helpers.js'
// Discovery is injected: tests announce units directly.
class FakeDiscovery extends EventEmitter {
constructor () { super(); this.available = true; this.reason = '' }
start () {}
stop () {}
announce (fake) {
this.emit('up', { name: fake.name, host: fake.name + '.local', addr: '127.0.0.1', port: fake.port, proto: 2 })
}
}
class MemStore {
constructor (data = {}) { this.data = data; this.saves = 0 }
load () { return this.data }
save (data) { this.data = JSON.parse(JSON.stringify(data)); this.saves++ }
}
function makeRig (store = new MemStore()) {
const discovery = new FakeDiscovery()
const rig = new Rig({ store, discovery, readbackMs: 100, unitOpts: { backoffStartMs: 100 } })
rig.start()
return { rig, discovery, store }
}
const pwms = (fake) => fake.lines.filter((l) => l.startsWith('pwm'))
test('starts disconnected; a discovered unit is listed, unticked, and untouched', async (t) => {
const fake = await new FakeUnit('u1').listen()
const { rig, discovery } = makeRig()
discovery.announce(fake)
const u = rig.snapshot().units.find((x) => x.id === 'u1')
t.ok(u, 'listed')
t.is(u.enabled, false, 'new units start unticked')
t.is(rig.snapshot().connected, false)
rig.setConnected(true)
await sleep(300)
t.is(fake.accepted, 0, 'unticked unit never contacted')
rig.stop()
await fake.close()
})
test('first connect adopts the unit\'s actual look instead of overwriting it', async (t) => {
const fake = await new FakeUnit('u1').listen()
fake.values = [65535, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // slot 1 blue, as Blender left it
const { rig, discovery } = makeRig()
discovery.announce(fake)
rig.updateUnit('u1', { enabled: true })
rig.setConnected(true)
t.ok(await waitFor(() => rig.snapshot().units[0].slots !== null), 'slots adopted from state')
const s = rig.snapshot().units[0].slots[0]
t.alike([s.r, s.g, s.b, s.level], [0, 0, 1, 1], 'slot 1 reads as full blue')
t.is(pwms(fake).length, 0, 'nothing pushed over the existing look')
rig.stop()
await fake.close()
})
test('editing a slot while connected drives the unit', async (t) => {
const fake = await new FakeUnit('u1').listen()
const { rig, discovery } = makeRig()
discovery.announce(fake)
rig.updateUnit('u1', { enabled: true, fadeMs: 0 })
rig.setConnected(true)
await waitFor(() => rig.snapshot().units[0].slots !== null)
rig.setSlot('u1', 1, { r: 1, g: 0, b: 0, level: 1 })
t.ok(await waitFor(() => pwms(fake).some((l) => l === 'pwm 0 0 0 0 0 30000 0 0 0 0 0 0')), 'slot 2 red at the 30000 cap')
rig.stop()
await fake.close()
})
test('disconnect closes every unit; edits while disconnected send nothing', async (t) => {
const fake = await new FakeUnit('u1').listen()
const { rig, discovery } = makeRig()
discovery.announce(fake)
rig.updateUnit('u1', { enabled: true })
rig.setConnected(true)
await waitFor(() => fake.conns.size === 1)
rig.setConnected(false)
t.ok(await waitFor(() => fake.conns.size === 0), 'closed')
const before = fake.lines.length
rig.setSlot('u1', 0, { r: 1, g: 1, b: 1, level: 1 })
await sleep(200)
t.is(fake.lines.length, before, 'nothing sent')
rig.stop()
await fake.close()
})
test('blackout while disconnected is one-shot and darkens the model', async (t) => {
const fake = await new FakeUnit('u1').listen()
const { rig, discovery } = makeRig()
discovery.announce(fake)
rig.updateUnit('u1', { slots: [{ r: 1, g: 1, b: 1, level: 1 }, null, null, null] })
rig.blackout()
t.ok(await waitFor(() => fake.lines.includes('stopfade')), 'stopfade sent')
t.ok(await waitFor(() => fake.conns.size === 0), 'released')
t.is(rig.snapshot().units[0].slots[0].level, 0, 'model shows dark')
rig.stop()
await fake.close()
})
test('settings survive a restart; connection state does not', async (t) => {
const fake = await new FakeUnit('u1').listen()
const store = new MemStore()
const a = makeRig(store)
a.discovery.announce(fake)
a.rig.updateUnit('u1', { enabled: true, label: 'Studio', redMax: 20000 })
a.rig.setConnected(true)
a.rig.saveLook('warm')
a.rig.flushSave()
a.rig.stop()
const b = makeRig(store)
const u = b.rig.snapshot().units.find((x) => x.id === 'u1')
t.is(u.label, 'Studio')
t.is(u.redMax, 20000)
t.is(u.enabled, true)
t.is(b.rig.snapshot().connected, false, 'a restart comes back disconnected')
t.alike(b.rig.snapshot().looks, ['warm'])
b.rig.stop()
await fake.close()
})
test('recalling a look fades every unit in it', async (t) => {
const fake = await new FakeUnit('u1').listen()
const { rig, discovery } = makeRig()
discovery.announce(fake)
rig.updateUnit('u1', { enabled: true, slots: [{ r: 0, g: 1, b: 0, level: 1 }, null, null, null] })
rig.saveLook('green')
rig.updateUnit('u1', { slots: [{ r: 0, g: 0, b: 0, level: 0 }, null, null, null] })
rig.setConnected(true)
await waitFor(() => fake.conns.size === 1)
rig.recallLook('green', 2000)
t.ok(await waitFor(() => fake.lines.some((l) => l.startsWith('pwm2 1000 2 0 65535 0'))), 'pwm2 over 2 s to green')
rig.stop()
await fake.close()
})