package ui import ( "strings" "testing" ) // spotifyRowDump is the shape of a real Spotify search-results screen, trimmed // to the rows that matter. The first row is a search *suggestion* (one line of // text); the rest are result rows (title + subtitle). const spotifyRowDump = ` ` const spotifyRowID = "com.spotify.music:id/row_root" func spotifyRows(t *testing.T) *Hierarchy { t.Helper() h, err := parseHierarchy([]byte(spotifyRowDump)) if err != nil { t.Fatalf("parseHierarchy: %v", err) } return h } func TestBestRowPicksTheTopResultOfARealScreen(t *testing.T) { h := spotifyRows(t) row, err := h.BestRow("Rolling Stones Honky Tonk", spotifyRowID, 1080, 2400) if err != nil { t.Fatalf("BestRow: %v", err) } if row.Title != "Honky Tonk Women" { t.Fatalf("chose row %q, want the canonical top hit; rows: %v", row.Title, row.Texts) } // The tap must land on the row, on the title, not on a row button. if row.Centre.X < 200 || row.Centre.X > 828 { t.Fatalf("tap x=%d is outside the title", row.Centre.X) } if row.Centre.Y < 794 || row.Centre.Y > 962 { t.Fatalf("tap y=%d is outside the row", row.Centre.Y) } } func TestBestRowIgnoresSearchSuggestions(t *testing.T) { h := spotifyRows(t) // The suggestion row carries every query word, but it is a single line of // text, so it is not a result row. query := "rolling stones honky tonk women single" row, err := h.BestRow(query, spotifyRowID, 1080, 2400) if err != nil { t.Fatalf("BestRow: %v", err) } if row.Title == "rolling stones honky tonk women single" { t.Fatal("chose the search suggestion; a suggestion has no subtitle and is not a result") } } func TestBestRowIgnoresRowsClippedByTheScreenEdge(t *testing.T) { // The last row is cut off at the bottom of the list: only 33 pixels tall, // with its centre underneath the navigation area. It matches the query // better than any other row, and it must still lose, because tapping it // would press something the user cannot see. h := spotifyRows(t) row, err := h.BestRow("Honky Tonk Women Live", spotifyRowID, 1080, 2400) if err != nil { t.Fatalf("BestRow: %v", err) } if row.Title == "Honky Tonk Women - Live" { t.Fatal("chose a row clipped by the bottom of the list") } } func TestBestRowRefusesAnUnknownQuery(t *testing.T) { h := spotifyRows(t) if _, err := h.BestRow("Chopin Nocturne", spotifyRowID, 1080, 2400); err == nil { t.Fatal("expected an error when no row matches the query") } } func TestBestRowWorksWithoutARowID(t *testing.T) { h := spotifyRows(t) row, err := h.BestRow("The Rolling Stones Honky Tonk Women", "", 1080, 2400) if err != nil { t.Fatalf("BestRow without a row id: %v", err) } if row.Title != "Honky Tonk Women" { t.Fatalf("chose %q", row.Title) } } func TestRowScorePrefersPrecision(t *testing.T) { exact, _ := rowScore(tokenize("honky tonk women"), []string{"Honky Tonk Women", "Song • The Rolling Stones"}) noisy, _ := rowScore(tokenize("honky tonk women"), []string{"Honky Tonk Women Live Licks Tour 2009 Remastered Deluxe Edition", "Song • The Rolling Stones, Sheryl Crow"}) if exact <= noisy { t.Fatalf("an exact title (%f) must beat a noisy one (%f)", exact, noisy) } } // TestBestRowPrefersTheTopHitWhenItsRowCarriesStateText is the regression that // cost the most: the top hit's row carries a "Paused" state indicator once it is // the loaded track, and counting that description as row text pushed the score // below an identical duplicate further down the list. tap_row then pressed the // duplicate at y=1443 and playback never started. func TestBestRowPrefersTheTopHitWhenItsRowCarriesStateText(t *testing.T) { h := spotifyRows(t) row, err := h.BestRow("Rolling Stones Honky Tonk", spotifyRowID, 1080, 2400) if err != nil { t.Fatalf("BestRow: %v", err) } if row.Title != "Honky Tonk Women" { t.Fatalf("chose %q", row.Title) } if row.Top != 794 { t.Fatalf("chose the row at y=%d, want the top hit at y=794", row.Top) } if row.Centre.Y != (794+962)/2 { t.Fatalf("tap point y=%d is not the top row's centre", row.Centre.Y) } } func TestRowTextsIgnoreStateDescriptions(t *testing.T) { h := spotifyRows(t) var top *Node h.Walk(func(n *Node) { if n.ResourceID == spotifyRowID && n.Bounds == "[0,794][1080,962]" { top = n } }) if top == nil { t.Fatal("top row not found in the sample dump") } for _, line := range rowTexts(top) { if line == "Paused" { t.Fatal("the row's state description must not count as row text") } } } func TestRowTextsSkipActionButtons(t *testing.T) { h := spotifyRows(t) // The suggestion row: one line of text plus an "Add suggestion" button. var suggestion *Node h.Walk(func(n *Node) { if n.ResourceID == spotifyRowID && n.Bounds == "[0,251][1080,425]" { suggestion = n } }) if suggestion == nil { t.Fatal("suggestion row not found in the sample dump") } texts := rowTexts(suggestion) if len(texts) != 1 { t.Fatalf("a suggestion row's own text = %v, want 1 line", texts) } // A result row: title + subtitle, without the row's action buttons. var result *Node h.Walk(func(n *Node) { if n.ResourceID == spotifyRowID && n.Bounds == "[0,794][1080,962]" { result = n } }) texts = rowTexts(result) if len(texts) != 2 || texts[0] != "Honky Tonk Women" || texts[1] != "Song • The Rolling Stones" { t.Fatalf("a result row's own text = %v", texts) } for _, bad := range texts { if strings.Contains(bad, "More options") || strings.Contains(bad, "Add ") { t.Fatalf("row text %q came from an action button", bad) } } } func TestTokenize(t *testing.T) { got := tokenize("Honky Tonk Women - Mono Version (Remastered)") want := []string{"honky", "tonk", "women", "mono", "version", "remastered"} if len(got) != len(want) { t.Fatalf("tokenize = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("tokenize = %v, want %v", got, want) } } }