feat(home): Songs-like → dedicated row + wider spread (#1491)
Promote the best-performing surface ("Songs like {artist}", ~8% skip /
~86% completion) out of the shared Playlists carousel into its own Home
row on both Android and web, and widen the daily build from 3 to 6 mixes
so the dedicated row shows a wider spread.
Server (internal/playlists):
- PickSeedArtists candidate pool 5 → 12; pickSeedArtistsForDay now takes
songsLikeSeedCount (6) instead of a hardcoded 3. Graceful degradation
and daily rotation preserved.
Android (HomeScreen.kt):
- New songsLikeSection + buildSongsLikeRow; PlaylistsRow takes a title so
it renders both the "Playlists" and "Songs like…" rows. buildOnlineRow
/ orderedRealPlaylists no longer reserve the 3 songs-like slots.
Offline shows cached mixes (available-first), hides the row when none.
Web (+page.svelte):
- Dedicated "Songs like…" row from songsLikeRow; dropped the 3-slot cap
and removed songs-like from the Playlists carousel.
Tests: seed_selection_test.go, BuildPlaylistsRowTest.kt, page.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { render, screen, within } from '@testing-library/svelte';
|
||||
import { readable } from 'svelte/store';
|
||||
import { emptyLikesMock } from '../test-utils/mocks/likes';
|
||||
import type { Playlist } from '$lib/api/types';
|
||||
@@ -57,7 +57,8 @@ afterEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('home Playlists section', () => {
|
||||
test('renders 5 placeholder cards when no playlists exist', () => {
|
||||
// Slot layout: For-You, Discover, 3× Songs-like.
|
||||
// For-You + Discover in the Playlists row, plus 3 in the dedicated
|
||||
// Songs-like row (#1491) = 5 placeholders total.
|
||||
render(Page);
|
||||
const placeholders = screen.queryAllByTestId('playlist-placeholder-card');
|
||||
expect(placeholders).toHaveLength(5);
|
||||
@@ -107,12 +108,52 @@ describe('home Playlists section', () => {
|
||||
render(Page);
|
||||
expect(screen.getByText('For You')).toBeInTheDocument();
|
||||
const placeholders = screen.queryAllByTestId('playlist-placeholder-card');
|
||||
// 1 Discover + 3 Songs-like slots = 4 placeholders alongside the
|
||||
// real For-You tile.
|
||||
// 1 Discover placeholder (Playlists row) + 3 Songs-like placeholders
|
||||
// (dedicated row) = 4 alongside the real For-You tile.
|
||||
expect(placeholders).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('renders secondary system kinds (deep_cuts / new_for_you) after Songs-like slots', () => {
|
||||
test('dedicated Songs-like row shows every generated mix, uncapped and out of Playlists', () => {
|
||||
const makeSongsLike = (id: string, name: string): Playlist => ({
|
||||
id,
|
||||
user_id: 'u1',
|
||||
owner_username: 'u1',
|
||||
name,
|
||||
description: '',
|
||||
is_public: false,
|
||||
kind: 'system',
|
||||
system_variant: 'songs_like_artist',
|
||||
refreshable: false,
|
||||
seed_artist_id: 'a1',
|
||||
cover_url: '',
|
||||
track_count: 25,
|
||||
duration_sec: 1500,
|
||||
created_at: '2026-05-04T00:00:00Z',
|
||||
updated_at: '2026-05-04T00:00:00Z'
|
||||
});
|
||||
// Six generated mixes — the old carousel capped at 3; the dedicated row shows all.
|
||||
const owned = Array.from({ length: 6 }, (_, i) => makeSongsLike(`sl${i}`, `Songs like ${i}`));
|
||||
(createPlaylistsQuery as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
(kind?: string) =>
|
||||
kind === 'system'
|
||||
? readable({ data: { owned, public: [] }, isPending: false, isError: false })
|
||||
: readable({ data: emptyPlaylistsResponse, isPending: false, isError: false })
|
||||
);
|
||||
const { container } = render(Page);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
expect(screen.getByText(`Songs like ${i}`)).toBeInTheDocument();
|
||||
}
|
||||
// The mixes live in the Songs-like row, not the Playlists carousel.
|
||||
const playlistsSection = container.querySelector('[aria-label="Playlists"]') as HTMLElement;
|
||||
expect(within(playlistsSection).queryByText('Songs like 0')).toBeNull();
|
||||
const songsSection = container.querySelector('[aria-label="Songs like"]') as HTMLElement;
|
||||
expect(within(songsSection).getByText('Songs like 0')).toBeInTheDocument();
|
||||
// No placeholders once real mixes exist.
|
||||
expect(within(songsSection).queryByTestId('playlist-placeholder-card')).toBeNull();
|
||||
});
|
||||
|
||||
test('renders secondary system kinds (deep_cuts / new_for_you) in the Playlists row', () => {
|
||||
// Operator backflow 2026-06-01: web Home surfaces the 5 secondary
|
||||
// system kinds when generated. No placeholders for them — they
|
||||
// depend on library shape, so missing means "not enough data."
|
||||
|
||||
Reference in New Issue
Block a user