package display import ( "strings" "testing" "git.piksar.eu/piotr/groundstation/internal/osd" ) // stubSource serves one fixed snapshot state, or none. type stubSource struct { snapshot osd.Snapshot state osd.SnapshotState } func (s stubSource) Latest() (osd.Snapshot, osd.SnapshotState) { return s.snapshot, s.state } // pendingSource is the contract state while the adaptive-link producers do // not exist: arm state and CH6 ceiling from the snapshot, everything else // explicitly pending. func pendingSource(armed bool, ceiling uint8) stubSource { return stubSource{snapshot: osd.Snapshot{ Armed: armed, OpConfig: osd.OperatorConfig{MaxPower: ceiling}, Policy: osd.GSPolicy{Pending: true}, Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending}, Link: osd.GSObservedLink{Known: false}, }, state: osd.SnapshotFresh} } func assertLines(t *testing.T, canvas *fakeCanvas, want []string) { t.Helper() lines := canvas.lines() if len(lines) < len(want) { t.Fatalf("lines=%q want at least %q", lines, want) } // Compare only the most recent draw pass: the canvas accumulates lines // across passes. lines = lines[len(lines)-len(want):] for i := range want { if lines[i] != want[i] { t.Fatalf("line %d=%q want %q", i, lines[i], want[i]) } } } func TestOSDTemplateRendersExplicitNoDataState(t *testing.T) { // Before any snapshot has ever existed the overlay must show explicit // unknown state plus the authoritative video state instead of a blank // overlay — including over the no-video fallback presentation. canvas := &fakeCanvas{width: 800, height: 600} NewOSDTemplate(NewVideoPresence(), stubSource{}).Draw(canvas) clears, flushes := canvas.drawCounts() if clears != 1 || flushes != 1 { t.Fatalf("clears=%d flushes=%d want 1 and 1", clears, flushes) } assertLines(t, canvas, []string{ "NO DATA", "VTX tbl:-- ceil:-- req:-- ack:--", "VIDEO OFF", }) } func TestOSDTemplateDrawsFreshSnapshotWithVideoOff(t *testing.T) { // Fresh snapshot data renders on top of the no-video fallback // presentation; the observed video lifecycle shows as VIDEO OFF. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() NewOSDTemplate(presence, pendingSource(true, 63)).Draw(canvas) assertLines(t, canvas, []string{ "ARM ARMED", "VTX tbl:-- ceil:63 req:-- ack:--", "VIDEO OFF", }) } func TestOSDTemplateMarksStaleLastKnownData(t *testing.T) { // A snapshot beyond the freshness window stays visible as last-known // data, explicitly marked stale — never presented as current. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ Armed: true, OpConfig: osd.OperatorConfig{ Table: osd.Table{Gen: 3, Hash: 0xcafe}, MaxPower: 63, }, Policy: osd.GSPolicy{Level: 42}, Ack: osd.VTXAck{ Table: osd.Table{Gen: 3}, Level: 40, Lifecycle: osd.VTXLifecycleApplied, }, }, state: osd.SnapshotStale}).Draw(canvas) assertLines(t, canvas, []string{ "ARM ARMED", "VTX tbl:3 ceil:63 req:42 ack:40", "DATA STALE", "VIDEO OFF", }) } func TestOSDTemplateStaysVisibleAcrossVideoTransition(t *testing.T) { // Video appearing fullscreen must not hide the OSD: the data lines stay // and only the authoritative VIDEO line flips. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() template := NewOSDTemplate(presence, pendingSource(true, 63)) template.Draw(canvas) assertLines(t, canvas, []string{ "ARM ARMED", "VTX tbl:-- ceil:63 req:-- ack:--", "VIDEO OFF", }) presence.SetActive(true) template.Draw(canvas) assertLines(t, canvas, []string{ "ARM ARMED", "VTX tbl:-- ceil:63 req:-- ack:--", "VIDEO ON", }) } func TestOSDTemplateRendersSelectedTableRequestedAndAppliedStates(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() presence.SetActive(true) NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ Armed: true, OpConfig: osd.OperatorConfig{ Table: osd.Table{Gen: 3, Hash: 0xcafe}, MaxPower: 127, }, Policy: osd.GSPolicy{Level: 42}, Ack: osd.VTXAck{ Table: osd.Table{Gen: 3}, Level: 40, Lifecycle: osd.VTXLifecycleApplied, }, }, state: osd.SnapshotFresh}).Draw(canvas) assertLines(t, canvas, []string{ "ARM ARMED", "VTX tbl:3 ceil:127 req:42 ack:40", "VIDEO ON", }) } func TestOSDTemplateRendersAckErrorState(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ OpConfig: osd.OperatorConfig{MaxPower: 10}, Policy: osd.GSPolicy{Pending: true}, Ack: osd.VTXAck{Lifecycle: osd.VTXLifecycleError}, }, state: osd.SnapshotFresh}).Draw(canvas) assertLines(t, canvas, []string{ "ARM OFF", "VTX tbl:-- ceil:10 req:-- ack:ERR", "VIDEO OFF", }) } func TestOSDTemplateRendersLevelZeroAsExplicitOff(t *testing.T) { // Level 0 in an available policy or acknowledgment is an explicit // off / no-runtime-command state and must render as 0; only a pending // value — no producer yet — renders as the pending mark. canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ OpConfig: osd.OperatorConfig{MaxPower: 0}, Policy: osd.GSPolicy{Level: 0}, Ack: osd.VTXAck{Level: 0, Lifecycle: osd.VTXLifecycleApplied}, }, state: osd.SnapshotFresh}).Draw(canvas) assertLines(t, canvas, []string{ "ARM OFF", "VTX tbl:-- ceil:0 req:0 ack:0", "VIDEO OFF", }) } func TestOSDTemplateVideoLineUsesOnlyObservedLifecycle(t *testing.T) { // The VIDEO line is sourced from the video lifecycle the display // observes on the display server, never from the snapshot's Video // field (the supervisor's launcher knowledge). canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() NewOSDTemplate(presence, stubSource{snapshot: osd.Snapshot{ Video: true, }, state: osd.SnapshotFresh}).Draw(canvas) for _, line := range canvas.lines() { if strings.HasPrefix(line, "VIDEO") && line != "VIDEO OFF" { t.Fatalf("line=%q: snapshot presence must never replace the observed lifecycle", line) } } } func TestOSDTemplateNeverDrawsTheFallbackText(t *testing.T) { canvas := &fakeCanvas{width: 800, height: 600} presence := NewVideoPresence() template := NewOSDTemplate(presence, pendingSource(true, 63)) for _, active := range []bool{false, true, false} { presence.SetActive(active) template.Draw(canvas) for _, line := range canvas.lines() { if line == wantFallbackText { t.Fatalf("the OSD is a telemetry surface, not a fallback: drew %q", line) } } } }