An audio player with transport, scrubber and volume; an equalizer and a visualizer; a playlist; a cassette, cartridge and record that turn while they play and hold where they stop; piano keys, a step sequencer and a tracker you can type into; a waveform, a 16:9 video frame, an image compare, a gallery, a filmstrip, a record button and an FM tuner. input exports SlideBase, the undrawn rail its sliders share; the gallery treats exported Bases as building blocks, not components. pixel's micro font gains '#' for notes like C#4. pixel.Pad and the slide rail read input only while their base is enabled, so Inert or a fresh Disabled stops a drag or a key already sent. AGENTS records why a drawn container keeps the DISABLED description: MyGo paints a disabled subtree at half opacity.
186 lines
5.6 KiB
Go
186 lines
5.6 KiB
Go
package media_test
|
||
|
||
import (
|
||
"bytes"
|
||
"log/slog"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/media"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
// inertView builds under Inert while *inert holds.
|
||
func inertView(w, h int, inert *bool, build func(c *ui.Context)) *ui.Tester {
|
||
clk := newWorld().clk
|
||
return ui.NewTester(func(c *ui.Context) {
|
||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: clk.now, Inert: *inert})
|
||
build(c)
|
||
}, w, h)
|
||
}
|
||
|
||
func TestScrubberKeepsAFractionalEnd(t *testing.T) {
|
||
pos, total := time.Duration(0), 1500*time.Millisecond
|
||
tt := view(400, 100, newWorld(), func(c *ui.Context) {
|
||
media.Scrubber(c, "Seek", &pos, total, media.ScrubberOptions{})
|
||
})
|
||
click(t, tt, "Seek")
|
||
tt.Key(0, ui.KeyEnd)
|
||
tt.Frame()
|
||
if pos != total {
|
||
t.Fatalf("End on a 1.5 s clip: at %v", pos)
|
||
}
|
||
}
|
||
|
||
func TestControlsHoldStillUnderInert(t *testing.T) {
|
||
inert := false
|
||
level, muted := 1, false
|
||
var struck []string
|
||
tt := inertView(500, 300, &inert, func(c *ui.Context) {
|
||
ui.Column(c).Children(func() {
|
||
media.Volume(c, "Volume", &level, &muted, media.VolumeOptions{})
|
||
struck = append(struck, media.PianoKeys(c, "Piano", media.PianoKeysOptions{}).Struck...)
|
||
})
|
||
})
|
||
// A press that began before Inert does not drag on.
|
||
r := find(t, tt, "Volume")
|
||
tt.Press(r.X+2, r.Y+r.H/2)
|
||
inert = true
|
||
tt.Frame()
|
||
tt.Move(r.X+r.W-1, r.Y+r.H/2)
|
||
tt.Release(r.X+r.W-1, r.Y+r.H/2)
|
||
if level != 0 {
|
||
t.Fatalf("an inert Volume dragged to %d", level)
|
||
}
|
||
p := find(t, tt, "Piano")
|
||
tt.Press(p.X+8, p.Y+p.H-6)
|
||
tt.Release(p.X+8, p.Y+p.H-6)
|
||
if len(struck) != 0 {
|
||
t.Fatalf("an inert piano struck %q", struck)
|
||
}
|
||
}
|
||
|
||
func TestStepSequencerIgnoresKeysOnceDisabled(t *testing.T) {
|
||
steps := make([][16]bool, 1)
|
||
disabled := false
|
||
tt := view(700, 200, newWorld(), func(c *ui.Context) {
|
||
media.StepSequencer(c, "Beat", []string{"KICK"}, steps, media.StepSequencerOptions{Disabled: disabled})
|
||
})
|
||
click(t, tt, "Beat")
|
||
steps[0] = [16]bool{}
|
||
// Disabled lands in the frame that reads Enter.
|
||
disabled = true
|
||
tt.Key(0, ui.KeyEnter)
|
||
if steps[0] != ([16]bool{}) {
|
||
t.Fatalf("a disabled sequencer toggled %v", steps[0])
|
||
}
|
||
}
|
||
|
||
func TestStepSequencerKeepsItsCursorAsTracksGo(t *testing.T) {
|
||
tracks, steps := []string{"KICK", "HAT"}, make([][16]bool, 2)
|
||
tt := view(700, 200, newWorld(), func(c *ui.Context) {
|
||
media.StepSequencer(c, "Beat", tracks, steps, media.StepSequencerOptions{})
|
||
})
|
||
click(t, tt, "Beat")
|
||
tt.Key(0, ui.KeyEnd)
|
||
tracks, steps = tracks[:1], steps[:1]
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyEnter)
|
||
if !steps[0][15] {
|
||
t.Fatalf("the cursor did not settle on the last step left: %v", steps[0])
|
||
}
|
||
}
|
||
|
||
func TestImageCompareRejectsASplitOutside(t *testing.T) {
|
||
for _, s := range []float64{-0.1, 2} {
|
||
mustPanic(t, "split outside 0–1", func() {
|
||
view(300, 200, newWorld(), func(c *ui.Context) {
|
||
media.ImageCompare(c, "C", solid(2, 2, pixel.Ink), solid(2, 2, pixel.Ink), &s, media.ImageCompareOptions{})
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestComposedControlsLogTheirOwnEvents(t *testing.T) {
|
||
var buf bytes.Buffer
|
||
old := slog.Default()
|
||
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||
defer slog.SetDefault(old)
|
||
pos, sel, rec := time.Duration(0), 0, false
|
||
tt := view(600, 400, newWorld(), func(c *ui.Context) {
|
||
ui.Column(c).Children(func() {
|
||
media.Scrubber(c, "Seek", &pos, time.Minute, media.ScrubberOptions{})
|
||
media.Playlist(c, "List", songs, 0, &sel, media.PlaylistOptions{Height: 60})
|
||
media.RecordButton(c, "Record", &rec, media.RecordButtonOptions{})
|
||
})
|
||
})
|
||
click(t, tt, "Seek")
|
||
click(t, tt, "Two, Bob")
|
||
tt.Key(0, ui.KeyEnter)
|
||
click(t, tt, "Record, 0:00")
|
||
for _, want := range []string{"8bit: scrubber", "8bit: playlist", "submitted=true", "8bit: record button"} {
|
||
if !strings.Contains(buf.String(), want) {
|
||
t.Errorf("no %q in the log:\n%s", want, buf.String())
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTrackerHoldsStillUnderInert(t *testing.T) {
|
||
inert := false
|
||
rows, cur := make([]media.Row, 4), 0
|
||
rows[1] = media.Row{Note: "E-4", Instrument: 1, Volume: 64}
|
||
tt := inertView(300, 200, &inert, func(c *ui.Context) {
|
||
media.Tracker(c, "Pattern", rows, &cur, media.TrackerOptions{})
|
||
})
|
||
click(t, tt, "Pattern")
|
||
tt.Key(0, ui.KeyHome)
|
||
// Inert lands in the frame that reads the keys.
|
||
inert = true
|
||
tt.Key(0, ui.KeyA)
|
||
tt.Key(0, ui.KeyDown)
|
||
tt.Key(0, ui.KeyDelete)
|
||
if rows[0].Note != "" || rows[1].Note != "E-4" || cur != 0 {
|
||
t.Fatalf("an inert tracker took keys: %+v, cursor %d", rows, cur)
|
||
}
|
||
}
|
||
|
||
func TestScrubberStepsAShortClip(t *testing.T) {
|
||
pos, n := time.Duration(0), 0
|
||
tt := view(400, 100, newWorld(), func(c *ui.Context) {
|
||
if media.Scrubber(c, "Seek", &pos, 5*time.Second, media.ScrubberOptions{}).Changed {
|
||
n++
|
||
}
|
||
})
|
||
click(t, tt, "Seek")
|
||
tt.Key(0, ui.KeyHome)
|
||
n = 0
|
||
tt.Key(0, ui.KeyRight)
|
||
if pos != 250*time.Millisecond || n != 1 {
|
||
t.Fatalf("Right on 5 s: at %v after %d changes", pos, n)
|
||
}
|
||
for range 19 {
|
||
tt.Key(0, ui.KeyRight)
|
||
}
|
||
if pos != 5*time.Second || n != 20 {
|
||
t.Fatalf("twenty Rights: at %v after %d changes", pos, n)
|
||
}
|
||
}
|
||
|
||
func TestPlaylistAndVideoFrameRejectBadSizes(t *testing.T) {
|
||
mustPanic(t, "a track of negative length", func() {
|
||
view(500, 300, newWorld(), func(c *ui.Context) {
|
||
s := 0
|
||
media.Playlist(c, "List", []media.Track{{Title: "Back", Artist: "Time", Length: -time.Second}}, -1, &s, media.PlaylistOptions{})
|
||
})
|
||
})
|
||
mustPanic(t, "a screen too narrow for its scrubber", func() {
|
||
view(500, 300, newWorld(), func(c *ui.Context) {
|
||
p, at := false, time.Duration(0)
|
||
media.VideoFrame(c, "Clip", &p, &at, time.Second, media.VideoFrameOptions{Width: 100}, func() {})
|
||
})
|
||
})
|
||
}
|