summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-25 23:09:20 +0300
committerPaul Buetow <paul@buetow.org>2026-04-25 23:09:20 +0300
commitb163e32473aba57936283a39c5d62089bebab46d (patch)
treee044fd4ec49bbeade50628618100f6a11d4d8b90
parent4113718e472116ef73b79701bde41014481cee50 (diff)
Add ambient music and wild-mode tests
- generator_test.go: - TestThemeSoundPresetsAmbientPopulated: every theme has Ambient.Normal and Ambient.Wild with at least one drone or pulse frequency. - TestThemeSoundPresetsAmbientValuesBounded: gains/durations/intervals are positive and below conservative max values. - TestThemeSoundsJSON_neonAmbientRoundTrip: themeSoundsJSON unmarshals and contains ambient.normal and ambient.wild. - integration_test.go: - TestKeyboardNavJS: verify p=ambient, f=flash hotkeys and nav hints. - TestIndexHTMLBakesSounds: verify index.html bakes window.SNONUX_SOUNDS. - TestThemeSelection: dynamically test all registered themes and assert generated sounds.json includes ambient data for each.
-rw-r--r--integrationtests/integration_test.go60
-rw-r--r--internal/generator/generator_test.go103
2 files changed, 158 insertions, 5 deletions
diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go
index 4b96d4d..9beaf29 100644
--- a/integrationtests/integration_test.go
+++ b/integrationtests/integration_test.go
@@ -4,6 +4,7 @@
package integrationtests
import (
+ "encoding/json"
"encoding/xml"
"fmt"
"image"
@@ -352,7 +353,8 @@ func TestInputCleanup(t *testing.T) {
}
}
-// TestKeyboardNavJS verifies that the generated HTML includes navigation attributes.
+// TestKeyboardNavJS verifies that the generated HTML includes navigation attributes
+// and that the shared JS binds the correct hotkeys.
func TestKeyboardNavJS(t *testing.T) {
inputDir, outputDir := makeDirs(t)
@@ -369,15 +371,43 @@ func TestKeyboardNavJS(t *testing.T) {
assertContains(t, sharedCSS, `.post-active`, "shared.css .post-active rule")
sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
assertContains(t, sharedJS, `playNavSound`, "shared.js playNavSound function")
+
+ // Final shortcut mapping: p = ambient playback start/pause, f = flash.
+ assertContains(t, sharedJS, "case 'p':", "shared.js p key handler")
+ assertContains(t, sharedJS, "toggleAmbientMode();", "shared.js p toggles ambient")
+ assertContains(t, sharedJS, "case 'f':", "shared.js f key handler")
+ assertContains(t, sharedJS, "triggerFlashEffect();", "shared.js f triggers flash")
+
+ // Nav hints and splash hints should display the updated keys.
+ assertContains(t, index, "<kbd>p</kbd> music", "index.html nav hint p=ambient")
+ assertContains(t, index, "<kbd>f</kbd> flash", "index.html nav hint f=flash")
+}
+
+// TestIndexHTMLBakesSounds verifies that the generated index.html bakes the
+// default theme's sounds into window.SNONUX_SOUNDS so the ambient engine can
+// start before any async theme fetches complete.
+func TestIndexHTMLBakesSounds(t *testing.T) {
+ inputDir, outputDir := makeDirs(t)
+
+ if err := os.WriteFile(filepath.Join(inputDir, "hello.txt"), []byte("sounds test"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ runPipeline(t, inputDir, outputDir)
+
+ index := readFile(t, filepath.Join(outputDir, "index.html"))
+ assertContains(t, index, "window.SNONUX_SOUNDS", "index.html bakes SNONUX_SOUNDS")
+ assertContains(t, index, `"ambient"`, "index.html sounds include ambient key")
+ assertContains(t, index, `"normal"`, "index.html sounds include ambient.normal")
+ assertContains(t, index, `"wild"`, "index.html sounds include ambient.wild")
}
// TestThemeSelection verifies that every registered theme renders a valid
// index.html containing core structural elements (post text, nav script).
func TestThemeSelection(t *testing.T) {
- themes := []string{
- "aurora", "brutalist", "cosmos", "matrix", "neon",
- "ocean", "plasma", "retro", "synthwave", "terminal", "volcano",
- "noir", "cathedral", "surveillance", "biomech",
+ themes := generator.ListThemes()
+ if len(themes) == 0 {
+ t.Fatal("no themes returned by ListThemes()")
}
for _, theme := range themes {
@@ -421,6 +451,26 @@ func TestThemeSelection(t *testing.T) {
}
}
+ // sounds.json must include ambient data.
+ soundsPath := filepath.Join(outputDir, "themes", theme, "sounds.json")
+ soundsData, err := os.ReadFile(soundsPath)
+ if err != nil {
+ t.Fatalf("read sounds.json: %v", err)
+ }
+ var sounds map[string]interface{}
+ if err := json.Unmarshal(soundsData, &sounds); err != nil {
+ t.Fatalf("sounds.json invalid JSON: %v", err)
+ }
+ ambient, ok := sounds["ambient"].(map[string]interface{})
+ if !ok {
+ t.Fatalf("sounds.json missing ambient object for theme %q", theme)
+ }
+ for _, key := range []string{"normal", "wild"} {
+ if _, ok := ambient[key]; !ok {
+ t.Errorf("sounds.json ambient missing %q variant for theme %q", key, theme)
+ }
+ }
+
// shared.js holds the nav logic.
sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
assertContains(t, sharedJS, "playNavSound", "shared.js playNavSound")
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index cfb9789..3ceb72c 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -145,6 +145,109 @@ func TestThemeSoundsJSON_ambientSchema(t *testing.T) {
}
}
+func TestThemeSoundPresetsAmbientPopulated(t *testing.T) {
+ t.Parallel()
+
+ for name := range themeSet {
+ preset, ok := themeSoundPresets[name]
+ if !ok {
+ t.Errorf("theme %q missing from themeSoundPresets", name)
+ continue
+ }
+
+ normal := preset.Ambient.Normal
+ wild := preset.Ambient.Wild
+
+ if len(normal.DroneFreqs) == 0 && len(normal.PulseFreqs) == 0 {
+ t.Errorf("theme %q ambient.Normal has no drone or pulse frequencies", name)
+ }
+ if len(wild.DroneFreqs) == 0 && len(wild.PulseFreqs) == 0 {
+ t.Errorf("theme %q ambient.Wild has no drone or pulse frequencies", name)
+ }
+ }
+}
+
+func TestThemeSoundPresetsAmbientValuesBounded(t *testing.T) {
+ t.Parallel()
+
+ for name := range themeSet {
+ preset, ok := themeSoundPresets[name]
+ if !ok {
+ continue
+ }
+
+ for _, mode := range []string{"normal", "wild"} {
+ var a ambientPreset
+ if mode == "normal" {
+ a = preset.Ambient.Normal
+ } else {
+ a = preset.Ambient.Wild
+ }
+
+ if a.Gain <= 0 || a.Gain > 0.15 {
+ t.Errorf("theme %q ambient.%s gain=%f; want (0, 0.15]", name, mode, a.Gain)
+ }
+ if a.BPM <= 0 || a.BPM > 250 {
+ t.Errorf("theme %q ambient.%s bpm=%f; want (0, 250]", name, mode, a.BPM)
+ }
+ if a.PulseInterval < 0 || a.PulseInterval > 10 {
+ t.Errorf("theme %q ambient.%s pulseInterval=%f; want [0, 10]", name, mode, a.PulseInterval)
+ }
+ if a.Attack <= 0 || a.Attack > 5 {
+ t.Errorf("theme %q ambient.%s attack=%f; want (0, 5]", name, mode, a.Attack)
+ }
+ if a.Release <= 0 || a.Release > 5 {
+ t.Errorf("theme %q ambient.%s release=%f; want (0, 5]", name, mode, a.Release)
+ }
+ if a.NoiseGain < 0 || a.NoiseGain > 0.1 {
+ t.Errorf("theme %q ambient.%s noiseGain=%f; want [0, 0.1]", name, mode, a.NoiseGain)
+ }
+ if a.DetuneCents < 0 || a.DetuneCents > 50 {
+ t.Errorf("theme %q ambient.%s detuneCents=%f; want [0, 50]", name, mode, a.DetuneCents)
+ }
+ for i, f := range a.DroneFreqs {
+ if f <= 0 {
+ t.Errorf("theme %q ambient.%s droneFreqs[%d]=%f; want positive", name, mode, i, f)
+ }
+ }
+ for i, f := range a.PulseFreqs {
+ if f <= 0 {
+ t.Errorf("theme %q ambient.%s pulseFreqs[%d]=%f; want positive", name, mode, i, f)
+ }
+ }
+ if a.CutoffMin < 0 || a.CutoffMin > 10000 {
+ t.Errorf("theme %q ambient.%s cutoffMin=%f; want [0, 10000]", name, mode, a.CutoffMin)
+ }
+ if a.CutoffMax < 0 || a.CutoffMax > 10000 {
+ t.Errorf("theme %q ambient.%s cutoffMax=%f; want [0, 10000]", name, mode, a.CutoffMax)
+ }
+ }
+ }
+}
+
+func TestThemeSoundsJSON_neonAmbientRoundTrip(t *testing.T) {
+ t.Parallel()
+
+ j := themeSoundsJSON("neon")
+ var s themeSounds
+ if err := json.Unmarshal([]byte(j), &s); err != nil {
+ t.Fatalf("themeSoundsJSON(\"neon\") unmarshal error: %v", err)
+ }
+
+ if s.Ambient.Normal.Gain <= 0 {
+ t.Errorf("neon ambient.normal gain missing or non-positive: %f", s.Ambient.Normal.Gain)
+ }
+ if s.Ambient.Wild.Gain <= 0 {
+ t.Errorf("neon ambient.wild gain missing or non-positive: %f", s.Ambient.Wild.Gain)
+ }
+ if len(s.Ambient.Normal.DroneFreqs) == 0 && len(s.Ambient.Normal.PulseFreqs) == 0 {
+ t.Error("neon ambient.normal has no frequencies")
+ }
+ if len(s.Ambient.Wild.DroneFreqs) == 0 && len(s.Ambient.Wild.PulseFreqs) == 0 {
+ t.Error("neon ambient.wild has no frequencies")
+ }
+}
+
func TestFormatPostTime(t *testing.T) {
t.Parallel()