diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-25 23:09:20 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-25 23:09:20 +0300 |
| commit | b163e32473aba57936283a39c5d62089bebab46d (patch) | |
| tree | e044fd4ec49bbeade50628618100f6a11d4d8b90 | |
| parent | 4113718e472116ef73b79701bde41014481cee50 (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.go | 60 | ||||
| -rw-r--r-- | internal/generator/generator_test.go | 103 |
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() |
