Files
artemis_control/internal/ui/media_test.go
Sam Rolfe c178f92851 feat: confirm playback from the media session, and stop demoting the top row
Phase 3 DoD reached end to end: `artemis run spotify_play query="Rolling
Stones Honky Tonk"` played the right song in 13 steps / 35.3s, with playback
paused first so the pass proves the row tap started it.

Two things stopped it before that, both found on the real phone:

- The playback check asserted on an on-screen "Pause" label that Spotify
  never shows: its transport button keeps the label "Play" while music is
  playing. The recipe now ends with a `playing` step that reads the app's
  media session (state PLAYING) and confirms the track title contains the
  query words. A failure reports the state and the track it found.
- tap_row pressed the wrong row. Once a track is loaded its row carries a
  `Paused` state indicator, and counting that content description as row text
  dropped the top hit below an identical duplicate further down, so the tap
  went to y=1443 instead of y=878. Rows are now scored on their title and
  subtitle only (state lives in content descriptions), and scores within
  rowScoreTolerance are treated as equal so the higher row wins, because
  Spotify already ranks by relevance.

internal/ui/rows_test.go reproduces that results screen, including the state
indicator and the duplicate row.

docs/phase-3-spotify-recipe.md records the successful run, the independent
media-session and audio-output reads, and the failure that preceded it.
2026-10-16 14:32:24 +11:00

78 lines
3.2 KiB
Go

package ui
import "testing"
// A real (trimmed) `dumpsys media_session` capture, taken while Spotify was
// playing the track the recipe asked for. Note the transport button on screen
// still read "Play".
const mediaSessionDump = `MEDIA SESSION SERVICE (dumpsys media_session)
12 sessions listeners:
1: com.spotify.music/SpotifySession/1 (userId=0)
package=com.spotify.music
launchIntent=null
active=true
flags=3
rating type=2
controllers: 12
state=PlaybackState {state=PLAYING(3), position=606, buffered position=0, speed=1.0, updated=1207066569, actions=3025843, custom actions=[Action:mName='Start radio, mIcon=2131233633, mExtras=Bundle[mParcelledData.dataSize=144]], active item id=18, error=null}
audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_UNKNOWN flags=0x800(FLAG_MUTE_HAPTIC)
volumeType=LOCAL, controlType=ABSOLUTE, max=0, current=0
metadata: size=18, description=Honky Tonk Women, The Rolling Stones, Honky Tonk Women / You Can't Always Get What You Want
queueTitle=Play Queue, size=50
2: org.kde.kdeconnect_tp.media_session org.kde.kdeconnect_tp/org.kde.kdeconnect_tp.media_session/630 (userId=0)
package=org.kde.kdeconnect_tp
active=false
state=PlaybackState {state=PAUSED(2), position=881, buffered position=0, speed=0.0, updated=1206521908, actions=260, custom actions=[], active item id=-1, error=null}
metadata: size=0, description=, ,
`
func TestPlaybackStateReadsTheRightSession(t *testing.T) {
got := parsePlayback(mediaSessionDump, "com.spotify.music")
if got == nil || !got.Playing() {
t.Fatalf("spotify playback = %+v", got)
}
if got.Title != "Honky Tonk Women, The Rolling Stones, Honky Tonk Women / You Can't Always Get What You Want" {
t.Fatalf("title = %q", got.Title)
}
// The block is delimited by its package line: Spotify's state must not leak
// into another package and vice versa.
other := parsePlayback(mediaSessionDump, "org.kde.kdeconnect_tp")
if other.Playing() {
t.Fatalf("kdeconnect playback = %+v, want paused", other)
}
if other.Title != ", ," && other.Title != "" {
t.Fatalf("kdeconnect title = %q", other.Title)
}
missing := parsePlayback(mediaSessionDump, "com.example.absent")
if missing.Playing() || missing.State != "NONE" {
t.Fatalf("missing package = %+v, want state NONE", missing)
}
}
func TestMatchesText(t *testing.T) {
const meta = "Honky Tonk Women, The Rolling Stones, Honky Tonk Women / You Can't Always Get What You Want"
if matched, total := MatchesText(meta, "Rolling Stones Honky Tonk"); matched != 4 || total != 4 {
t.Fatalf("matched %d/%d, want 4/4", matched, total)
}
if matched, _ := MatchesText(meta, "Chopin Nocturne"); matched != 0 {
t.Fatalf("matched %d for an unrelated query, want 0", matched)
}
if _, total := MatchesText(meta, ""); total != 0 {
t.Fatalf("empty query should have no words, got %d", total)
}
}
func TestPlaybackString(t *testing.T) {
p := &Playback{Package: "com.spotify.music", State: "PLAYING", Title: "Honky Tonk Women"}
if s := p.String(); s != `state=PLAYING, playing "Honky Tonk Women"` {
t.Fatalf("String() = %q", s)
}
var nilSession *Playback
if s := nilSession.String(); s != "no media session" {
t.Fatalf("nil String() = %q", s)
}
}