feat(web): player bar, playable track lists and Collection (M3)
- player bar with cover, like and dislike, previous / play / next, shuffle, repeat, seek bar and volume; PlayerProvider lives with the signed-in shell and restores the last track and position paused - clicking a row (or Enter) plays the list from that track with its context (album, search, history, collection); the current track is highlighted and rows have a like button; a reaction refreshes lists - Collection: history grouped by day with clearing, likes with "play all" and dislikes, all with paging; sidebar section - events and playback state are flushed when the page is hidden; player timers survive StrictMode remounts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec3c2810c8
commit
976ffb574b
15 files changed
+803
-49
No files matched your search
@@ -245,6 +245,46 @@ describe('search box', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('player', () => {
|
||||
it('plays an album from a clicked row, likes and pauses with the keyboard', async () => {
|
||||
const alice = makeUser('alice', 'user');
|
||||
const { user, api } = await renderApp([alice], '/album/00000000-0000-4000-8000-0000000000bb', { signedInAs: alice });
|
||||
|
||||
const row = (await screen.findByText('Закрой за мной дверь')).closest('[role="row"]')!;
|
||||
await user.click(row);
|
||||
const bar = screen.getByRole('contentinfo', { name: 'Плеер' });
|
||||
await waitFor(() => expect(within(bar).getByText('Закрой за мной дверь')).toBeInTheDocument());
|
||||
expect(row).toHaveAttribute('aria-current', 'true');
|
||||
expect(within(bar).getByRole('button', { name: 'Пауза' })).toBeInTheDocument();
|
||||
|
||||
await user.click(within(bar).getByRole('button', { name: 'Нравится' }));
|
||||
await waitFor(() =>
|
||||
expect(api.calls).toContainEqual({
|
||||
method: 'PUT',
|
||||
path: '/api/v1/me/reactions/tracks/00000000-0000-4000-8000-000000000101',
|
||||
body: { reaction: 'like' },
|
||||
}),
|
||||
);
|
||||
expect(within(bar).getByRole('button', { name: 'Убрать из лайков' })).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// Пробел на кнопке нажимает её саму; горячая клавиша работает, когда фокус не на элементе управления.
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
await user.keyboard(' ');
|
||||
expect(within(bar).getByRole('button', { name: 'Слушать' })).toBeInTheDocument();
|
||||
|
||||
// Уход со страницы досылает события и сохраняет состояние плеера.
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
await waitFor(() => {
|
||||
const sent = api.calls.filter((c) => c.path === '/api/v1/events').flatMap((c) => (c.body as { events: { type: string; context_type?: string }[] }).events);
|
||||
expect(sent.find((e) => e.type === 'play_start')).toMatchObject({ context_type: 'album' });
|
||||
});
|
||||
expect(api.calls.find((c) => c.method === 'PUT' && c.path === '/api/v1/me/playback-state')?.body).toMatchObject({
|
||||
track_id: '00000000-0000-4000-8000-000000000101',
|
||||
queue_index: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeRedirect', () => {
|
||||
it.each([
|
||||
['/settings', '/settings'],
|
||||
|
||||
Reference in new issue
Block a user