package ui import ( "math" "strings" "testing" "time" "charm.land/lipgloss/v2" ) const classicPeakTestGlyphs = "⎺⎻⎼⎽" const classicPeakTestEpsilon = 1e-9 func withPanelWidth(t *testing.T, width int) { t.Helper() prevWidth := PanelWidth PanelWidth = width t.Cleanup(func() { PanelWidth = prevWidth }) } func uniformBands(level float64) []float64 { return uniformBandsN(DefaultSpectrumBands, level) } func uniformBandsN(count int, level float64) []float64 { bands := make([]float64, count) for i := range bands { bands[i] = level } return bands } func repeatedClassicPeakSlice(width int, level float64) []float64 { cols := classicPeakColsForWidth(width) out := make([]float64, cols) for i := range out { out[i] = level } return out } func activateMode(t *testing.T, v *Visualizer, mode VisMode) visModeDriver { t.Helper() v.Mode = mode driver := v.syncDriverMode() if driver == nil { t.Fatalf("syncDriverMode() returned nil for mode %v", mode) } return driver } func classicPeakDriverFor(t *testing.T, v *Visualizer) *classicPeakDriver { t.Helper() driver, ok := v.driverFor(VisClassicPeak).(*classicPeakDriver) if !ok || driver == nil { t.Fatal("driverFor(VisClassicPeak) did not return *classicPeakDriver") } return driver } func terrainDriverFor(t *testing.T, v *Visualizer) *terrainDriver { t.Helper() driver, ok := v.driverFor(VisTerrain).(*terrainDriver) if !ok || driver == nil { t.Fatal("driverFor(VisTerrain) did not return *terrainDriver") } return driver } func TestClassicPeakModeLookup(t *testing.T) { got, ok := StringToVisModeExact("ClassicPeak") if !ok || got != VisClassicPeak { t.Fatalf("StringToVisModeExact(ClassicPeak) = (%v, %v), want (%v, true)", got, ok, VisClassicPeak) } v := NewVisualizer(44100) v.Mode = VisClassicPeak if got := v.ModeName(); got != "ClassicPeak" { t.Fatalf("ModeName() = %q, want %q", got, "ClassicPeak") } } func TestVisualizerFrameAdvancesOnTickNotRender(t *testing.T) { v := NewVisualizer(44100) v.Analyze(make([]float64, defaultFFTSize), spectrumAnalysisSpec(DefaultSpectrumBands)) if v.frame != 0 { t.Fatalf("Analyze() advanced frame to %d, want 0 before tick", v.frame) } v.bands = uniformBands(0.5) v.Render() if v.frame != 0 { t.Fatalf("Render() advanced frame to %d, want 0 before tick", v.frame) } v.Tick(VisTickContext{}) if v.frame != 1 { t.Fatalf("Tick() advanced frame to %d, want 1", v.frame) } v.bands = uniformBands(0.5) v.Render() if v.frame != 1 { t.Fatalf("Render() advanced frame to %d after tick, want 1", v.frame) } } func TestClassicPeakLaunchAndSettle(t *testing.T) { withPanelWidth(t, 8) cols := classicPeakColsForWidth(PanelWidth) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.2) driver.sync(v) v.Render() if len(driver.barPos) != cols { t.Fatalf("initial bar count = %d, want %d", len(driver.barPos), cols) } if len(driver.peakPos) != cols { t.Fatalf("initial cap count = %d, want %d", len(driver.peakPos), cols) } for i, pos := range driver.barPos { if math.Abs(pos-0.2) > classicPeakTestEpsilon { t.Fatalf("initial bar[%d] = %v, want 0.2", i, pos) } } for i, pos := range driver.peakPos { if math.Abs(pos-0.2) > classicPeakTestEpsilon { t.Fatalf("initial cap[%d] = %v, want 0.2", i, pos) } if driver.peakVel[i] != 0 { t.Fatalf("initial velocity[%d] = %v, want 0", i, driver.peakVel[i]) } if driver.peakHold[i] != 0 { t.Fatalf("initial hold[%d] = %v, want 0", i, driver.peakHold[i]) } } v.bands = uniformBands(0.8) driver.sync(v) for i, pos := range driver.barPos { if math.Abs(pos-0.2) > classicPeakTestEpsilon { t.Fatalf("bars snapped on sync[%d] = %v, want 0.2", i, pos) } } for i, pos := range driver.peakPos { if math.Abs(pos-0.8) > classicPeakTestEpsilon { t.Fatalf("cap launch[%d] = %v, want 0.8", i, pos) } if driver.peakVel[i] <= 0 { t.Fatalf("velocity on sync[%d] = %v, want > 0", i, driver.peakVel[i]) } if driver.peakHold[i] != 0 { t.Fatalf("hold on sync[%d] = %v, want 0", i, driver.peakHold[i]) } } now := time.Now() driver.advance(v, now) for i, pos := range driver.barPos { if pos <= 0.2 || pos >= 0.8 { t.Fatalf("animated bar[%d] = %v, want between 0.2 and 0.8", i, pos) } if driver.peakPos[i] <= 0.8 { t.Fatalf("animated cap[%d] = %v, want > 0.8", i, driver.peakPos[i]) } if driver.peakPos[i] <= pos { t.Fatalf("animated cap[%d] = %v, want > bar %v", i, driver.peakPos[i], pos) } if driver.peakVel[i] <= 0 { t.Fatalf("launch velocity[%d] = %v, want > 0", i, driver.peakVel[i]) } } v.bands = uniformBands(0.3) driver.sync(v) for i, pos := range driver.barPos { if pos <= 0.3 { t.Fatalf("settling bar[%d] = %v, want > 0.3", i, pos) } if driver.peakPos[i] <= pos { t.Fatalf("settling cap[%d] = %v, want > bar %v", i, driver.peakPos[i], pos) } } for step := 1; step <= 128 && driver.animating(v); step++ { driver.advance(v, now.Add(time.Duration(step)*tickClassicPeak)) } if driver.animating(v) { t.Fatal("animating() = true after settle window, want false") } for i, pos := range driver.barPos { if math.Abs(pos-0.3) > classicPeakVisibleEpsilon { t.Fatalf("settled bar[%d] = %v, want about 0.3", i, pos) } } for i, pos := range driver.peakPos { if math.Abs(pos-0.3) > classicPeakVisibleEpsilon { t.Fatalf("settled cap[%d] = %v, want about 0.3", i, pos) } if driver.peakVel[i] != 0 { t.Fatalf("settled velocity[%d] = %v, want 0", i, driver.peakVel[i]) } if driver.peakHold[i] != 0 { t.Fatalf("settled hold[%d] = %v, want 0", i, driver.peakHold[i]) } } } func TestClassicPeakHangsBrieflyAtApex(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.2) driver.sync(v) v.Render() v.bands = uniformBands(0.8) driver.sync(v) now := time.Now() foundHold := false for step := 1; step <= 32; step++ { driver.advance(v, now.Add(time.Duration(step)*tickClassicPeak)) if driver.peakHold[0] <= 0 { continue } foundHold = true heldPos := driver.peakPos[0] heldVel := driver.peakVel[0] heldFor := driver.peakHold[0] driver.advance(v, now.Add(time.Duration(step+1)*tickClassicPeak)) if driver.peakPos[0] != heldPos { t.Fatalf("held cap moved: got %v, want %v", driver.peakPos[0], heldPos) } if driver.peakVel[0] != heldVel { t.Fatalf("held cap velocity changed: got %v, want %v", driver.peakVel[0], heldVel) } if driver.peakHold[0] >= heldFor { t.Fatalf("hold timer did not decrease: got %v, want < %v", driver.peakHold[0], heldFor) } break } if !foundHold { t.Fatal("peakHold never activated") } } func TestClassicPeakDoesNotRelaunchWhileAirborne(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.2) driver.sync(v) v.Render() v.bands = uniformBands(0.8) driver.sync(v) launchPos := append([]float64(nil), driver.peakPos...) launchVel := append([]float64(nil), driver.peakVel...) v.bands = uniformBands(0.95) driver.sync(v) for i, pos := range driver.peakPos { if pos != launchPos[i] { t.Fatalf("cap relaunched in air[%d] = %v, want %v", i, pos, launchPos[i]) } if driver.peakVel[i] != launchVel[i] { t.Fatalf("velocity changed in air[%d] = %v, want %v", i, driver.peakVel[i], launchVel[i]) } } } func TestClassicPeakResetsOnModeSwitchAndWidthChange(t *testing.T) { withPanelWidth(t, 6) cols6 := classicPeakColsForWidth(PanelWidth) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.4) driver.sync(v) v.Render() driver.barPos[1] = 0.7 driver.peakPos[1] = 0.9 driver.peakVel[1] = 1.2 activateMode(t, v, VisBars) activateMode(t, v, VisClassicPeak) driver.sync(v) if len(driver.barPos) != cols6 { t.Fatalf("reset bar len = %d, want %d", len(driver.barPos), cols6) } if len(driver.peakPos) != cols6 { t.Fatalf("reset cap len = %d, want %d", len(driver.peakPos), cols6) } for i, pos := range driver.barPos { if math.Abs(pos-0.4) > classicPeakTestEpsilon { t.Fatalf("reset bar[%d] = %v, want 0.4", i, pos) } } for i, pos := range driver.peakPos { if math.Abs(pos-0.4) > classicPeakTestEpsilon { t.Fatalf("reset cap[%d] = %v, want 0.4", i, pos) } if driver.peakVel[i] != 0 { t.Fatalf("reset velocity[%d] = %v, want 0", i, driver.peakVel[i]) } if driver.peakHold[i] != 0 { t.Fatalf("reset hold[%d] = %v, want 0", i, driver.peakHold[i]) } } PanelWidth = 8 cols8 := classicPeakColsForWidth(PanelWidth) driver.sync(v) if len(driver.barPos) != cols8 { t.Fatalf("resize bar len = %d, want %d", len(driver.barPos), cols8) } if len(driver.peakPos) != cols8 { t.Fatalf("resize cap len = %d, want %d", len(driver.peakPos), cols8) } for i, pos := range driver.barPos { if math.Abs(pos-0.4) > classicPeakTestEpsilon { t.Fatalf("resize bar[%d] = %v, want 0.4", i, pos) } } for i, pos := range driver.peakPos { if math.Abs(pos-0.4) > classicPeakTestEpsilon { t.Fatalf("resize cap[%d] = %v, want 0.4", i, pos) } if driver.peakVel[i] != 0 { t.Fatalf("resize velocity[%d] = %v, want 0", i, driver.peakVel[i]) } if driver.peakHold[i] != 0 { t.Fatalf("resize hold[%d] = %v, want 0", i, driver.peakHold[i]) } } } func TestClassicPeakAnimatingWhenCapIsAboveBar(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.3) driver.barPos = repeatedClassicPeakSlice(PanelWidth, 0.3) driver.peakPos = repeatedClassicPeakSlice(PanelWidth, 0.5) driver.peakVel = repeatedClassicPeakSlice(PanelWidth, 0) if !driver.animating(v) { t.Fatal("animating() = false, want true when caps are still above the bar") } } func TestClassicPeakAnimatingWhenBarsAreSettling(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.bands = uniformBands(0.7) driver.barPos = repeatedClassicPeakSlice(PanelWidth, 0.5) driver.peakPos = repeatedClassicPeakSlice(PanelWidth, 0.5) driver.peakVel = repeatedClassicPeakSlice(PanelWidth, 0) if !driver.animating(v) { t.Fatal("animating() = false, want true while bars are still easing to target") } } func TestClassicPeakRetainsDetailAtRightEdge(t *testing.T) { v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) bands := make([]float64, classicPeakSpectrumBands) copy(bands[len(bands)-6:], []float64{0.55, 0.46, 0.37, 0.28, 0.19, 0.1}) v.bands = bands levels := driver.levels(v) if len(levels) < 4 { t.Fatalf("levels len = %d, want at least 4", len(levels)) } foundStep := false for i := len(levels) - 4; i < len(levels)-1; i++ { if math.Abs(levels[i]-levels[i+1]) > 0.01 { foundStep = true break } } if !foundStep { t.Fatalf("right edge flattened unexpectedly: tail=%v", levels[len(levels)-4:]) } } func TestClassicPeakRenderHidesLandedCaps(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) v.Rows = 5 v.bands = uniformBands(0.6) out := v.Render() if strings.ContainsAny(out, classicPeakTestGlyphs) { t.Fatalf("Render() showed a cap even though peaks were seeded at the bar height: %q", out) } } func TestClassicPeakRenderShowsAttachedCapWhileSettling(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.Rows = 5 v.bands = uniformBands(0.61) driver.barPos = repeatedClassicPeakSlice(PanelWidth, 0.61) driver.peakPos = repeatedClassicPeakSlice(PanelWidth, 0.68) driver.peakVel = repeatedClassicPeakSlice(PanelWidth, 0) out := v.Render() if !strings.ContainsAny(out, classicPeakTestGlyphs) { t.Fatalf("Render() did not show an attached settling cap: %q", out) } } func TestClassicPeakPauseFreezesStateAndClearsAnimationClock(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.Rows = 5 v.bands = uniformBands(0.6) driver.barPos = repeatedClassicPeakSlice(PanelWidth, 0.6) driver.peakPos = repeatedClassicPeakSlice(PanelWidth, 0.82) driver.peakVel = repeatedClassicPeakSlice(PanelWidth, 1.1) driver.peakHold = repeatedClassicPeakSlice(PanelWidth, classicPeakApexHold) snapshotPeak := append([]float64(nil), driver.peakPos...) snapshotVel := append([]float64(nil), driver.peakVel...) snapshotHold := append([]float64(nil), driver.peakHold...) driver.lastTick = time.Now() v.Tick(VisTickContext{Now: time.Now(), Paused: true}) for i := range driver.peakPos { if driver.peakPos[i] != snapshotPeak[i] { t.Fatalf("paused cap[%d] = %v, want frozen %v", i, driver.peakPos[i], snapshotPeak[i]) } if driver.peakVel[i] != snapshotVel[i] { t.Fatalf("paused velocity[%d] = %v, want frozen %v", i, driver.peakVel[i], snapshotVel[i]) } if driver.peakHold[i] != snapshotHold[i] { t.Fatalf("paused hold[%d] = %v, want frozen %v", i, driver.peakHold[i], snapshotHold[i]) } } if !driver.animating(v) { t.Fatal("animating() = false, want true while caps still airborne") } if got := v.TickInterval(VisTickContext{Paused: true}); got != TickSlow { t.Fatalf("TickInterval(paused) = %v, want %v", got, TickSlow) } } func TestClassicPeakOverlayFreezesStateAndClearsAnimationClock(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) driver := classicPeakDriverFor(t, v) v.Rows = 5 v.bands = uniformBands(0.6) driver.barPos = repeatedClassicPeakSlice(PanelWidth, 0.6) driver.peakPos = repeatedClassicPeakSlice(PanelWidth, 0.82) driver.peakVel = repeatedClassicPeakSlice(PanelWidth, 1.1) driver.peakHold = repeatedClassicPeakSlice(PanelWidth, classicPeakApexHold) snapshotPeak := append([]float64(nil), driver.peakPos...) snapshotVel := append([]float64(nil), driver.peakVel...) snapshotHold := append([]float64(nil), driver.peakHold...) driver.lastTick = time.Now() driver.Tick(v, VisTickContext{Now: time.Now(), OverlayActive: true}) if !driver.lastTick.IsZero() { t.Fatalf("lastTick after overlay = %v, want zero", driver.lastTick) } for i := range driver.peakPos { if driver.peakPos[i] != snapshotPeak[i] { t.Fatalf("overlay cap[%d] = %v, want frozen %v", i, driver.peakPos[i], snapshotPeak[i]) } if driver.peakVel[i] != snapshotVel[i] { t.Fatalf("overlay velocity[%d] = %v, want frozen %v", i, driver.peakVel[i], snapshotVel[i]) } if driver.peakHold[i] != snapshotHold[i] { t.Fatalf("overlay hold[%d] = %v, want frozen %v", i, driver.peakHold[i], snapshotHold[i]) } } if !driver.animating(v) { t.Fatal("animating() = false, want true while overlay hides airborne caps") } if got := v.TickInterval(VisTickContext{OverlayActive: true}); got != TickSlow { t.Fatalf("TickInterval(overlay) = %v, want %v", got, TickSlow) } } func TestClassicPeakRenderFillsEvenWidthPanels(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) v.Rows = 5 v.bands = uniformBands(0.6) out := v.Render() for line := range strings.SplitSeq(out, "\n") { if got := lipgloss.Width(line); got != PanelWidth { t.Fatalf("Render() line width = %d, want %d for even panel width: %q", got, PanelWidth, line) } } } func TestClassicPeakEvenWidthKeepsBarsSeparated(t *testing.T) { withPanelWidth(t, 8) v := NewVisualizer(44100) activateMode(t, v, VisClassicPeak) v.Rows = 5 v.bands = uniformBands(0.6) out := strings.Split(v.Render(), "\n") if len(out) == 0 { t.Fatalf("Render() returned too few rows: %q", out) } found := false for _, line := range out { if strings.Contains(line, "█") { if strings.Contains(line, "███") { t.Fatalf("Render() merged adjacent bars on even widths: %q", line) } found = true break } } if !found { t.Fatalf("Render() did not render any bar rows: %q", out) } }