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.
This commit is contained in:
@@ -133,15 +133,22 @@ func (h *Hierarchy) rowCandidates(rowID string, screenW, screenH int) []*Node {
|
||||
return out
|
||||
}
|
||||
|
||||
// betterRow ranks rows: the best text match wins, and equal matches go to the
|
||||
// row nearest the top, because search results are ranked by relevance.
|
||||
// betterRow ranks rows. Spotify already ranks results by relevance, so a higher
|
||||
// row wins whenever the text match is comparable: the top hit can carry extra
|
||||
// state text ("Paused" on the row of the track that is already loaded) that
|
||||
// must not push it below a duplicate further down the list. Scores that are
|
||||
// further apart are not comparable, and the better text match wins.
|
||||
func betterRow(a, b *Row) bool {
|
||||
if math.Abs(a.Score-b.Score) > 1e-9 {
|
||||
if math.Abs(a.Score-b.Score) > rowScoreTolerance {
|
||||
return a.Score > b.Score
|
||||
}
|
||||
return a.Top < b.Top
|
||||
}
|
||||
|
||||
// rowScoreTolerance is how close two row scores must be before the higher row
|
||||
// is preferred.
|
||||
const rowScoreTolerance = 0.15
|
||||
|
||||
func rowMatchesID(n *Node, rowID string) bool {
|
||||
if rowID == "" {
|
||||
return true
|
||||
@@ -170,10 +177,12 @@ func textDescendants(n *Node) []string {
|
||||
// rowTexts returns the text that belongs to the row itself: its title and
|
||||
// subtitle.
|
||||
//
|
||||
// Anything inside a clickable child is an action button ("More options", "Add
|
||||
// to library", "Add suggestion"), not the row's own text, so that subtree is
|
||||
// skipped. This is what keeps a one-line search suggestion from looking like a
|
||||
// two-line result.
|
||||
// Two things are left out on purpose. Anything inside a clickable child is an
|
||||
// action button ("More options", "Add to library", "Add suggestion"), not the
|
||||
// row's own text, so that subtree is skipped. And content descriptions are not
|
||||
// counted, because that is where apps put *state* ("Paused", "Playing") rather
|
||||
// than the row's title; counting them made the row of the track that was
|
||||
// already loaded score worse than an identical duplicate further down.
|
||||
func rowTexts(n *Node) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
@@ -192,9 +201,6 @@ func rowTexts(n *Node) []string {
|
||||
return
|
||||
}
|
||||
add(c.Text)
|
||||
if !c.Clickable {
|
||||
add(c.ContentDesc)
|
||||
}
|
||||
for i := range c.Children {
|
||||
visit(&c.Children[i], false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user