feat(discover): explain the taste match on both clients — #2377 (clients)
"Matches your taste in shoegaze and dream pop." replaces the seed
attribution when the candidate's own tags overlap the taste profile.
The preference order is the point of slice 6: the tag reason describes the
MUSIC ("sounds like what you like"), while seed attribution describes the
graph ("adjacent to something you played"). When we can say the former, it
is strictly the better explanation. When we can't — the common case, since
tag coverage for out-of-library artists is partial by nature (#2376) — the
card falls back to attribution rather than going blank.
Both clients share the wording, Oxford comma included, and both have tests
asserting the exact strings. That's deliberate: identical copy across two
codebases silently diverges unless something fails when it does.
Android caps at 3 tags client-side even though the server already does.
The server contract could widen; a run-on subtitle shouldn't be how we
find out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,3 +244,49 @@ describe('SuggestionFeed snooze (#2375)', () => {
|
||||
expect(screen.queryByText(/listen to something or like an artist/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SuggestionFeed taste-tag reason (#2377)', () => {
|
||||
const tagged = (over: Partial<ArtistSuggestion> = {}): ArtistSuggestion => ({
|
||||
...oneSeed,
|
||||
mbid: 'mbT',
|
||||
name: 'Tag Match',
|
||||
...over
|
||||
});
|
||||
|
||||
test('matched tags replace seed attribution as the reason line', () => {
|
||||
setSuggestions([tagged({ matched_tags: ['shoegaze', 'dream pop'] })]);
|
||||
render(SuggestionFeed);
|
||||
expect(
|
||||
screen.getByText(/matches your taste in shoegaze and dream pop\./i)
|
||||
).toBeInTheDocument();
|
||||
// The graph-adjacency line is superseded — describing the music beats
|
||||
// describing the graph when we can do both.
|
||||
expect(screen.queryByText(/because you liked/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('one matched tag reads in the singular', () => {
|
||||
setSuggestions([tagged({ matched_tags: ['shoegaze'] })]);
|
||||
render(SuggestionFeed);
|
||||
expect(screen.getByText(/matches your taste in shoegaze\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('three matched tags use an Oxford comma, matching the seed copy', () => {
|
||||
setSuggestions([tagged({ matched_tags: ['a', 'b', 'c'] })]);
|
||||
render(SuggestionFeed);
|
||||
expect(screen.getByText(/matches your taste in a, b, and c\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The common case: most candidates have no cached tags, and the card must
|
||||
// still explain itself rather than going blank.
|
||||
test('no matched tags falls back to seed attribution', () => {
|
||||
setSuggestions([oneSeed]);
|
||||
render(SuggestionFeed);
|
||||
expect(screen.getByText(/because you liked seed\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('an empty matched_tags array also falls back, not to an empty line', () => {
|
||||
setSuggestions([tagged({ matched_tags: [] })]);
|
||||
render(SuggestionFeed);
|
||||
expect(screen.getByText(/because you liked seed\./i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user