feat(playlists): share playlists with revocable read-only links
ci / api / api (push) Successful in 55s
release / release (версия и заметки) (push) Successful in 5s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m21s
ci / ts / ts (packages + web) (push) Successful in 2m19s
release / android (APK в релиз) (push) Successful in 4m45s
release / desktop (Linux и Windows в релиз) (push) Successful in 20m58s
release / prune (старые файлы и прогоны) (push) Successful in 9s
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m35s
ci / db / db (push) Successful in 59s
ci / api / api (push) Successful in 55s
release / release (версия и заметки) (push) Successful in 5s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m21s
ci / ts / ts (packages + web) (push) Successful in 2m19s
release / android (APK в релиз) (push) Successful in 4m45s
release / desktop (Linux и Windows в релиз) (push) Successful in 20m58s
release / prune (старые файлы и прогоны) (push) Successful in 9s
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m35s
ci / db / db (push) Successful in 59s
This commit is contained in:
1 parent
bfcf6e48b2
commit
0a59059738
23 files changed
+1164
-319
No files matched your search
@@ -10,7 +10,7 @@ import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { App } from './App';
|
||||
import { createAppRouter, safeRedirect } from './app/router';
|
||||
import { fakeApi, makeUser, type FakeUser } from './test/fakeApi';
|
||||
import { albumTracks, fakeApi, makeUser, type FakeUser } from './test/fakeApi';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -506,3 +506,55 @@ describe('recommendation preferences', () => {
|
||||
expect(api.calls.filter((c) => c.path === '/api/v1/me/onboarding').at(-1)?.body).toEqual({ artist_ids: [], genres: [], gender: null, age_group: null });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('playlist sharing', () => {
|
||||
it('copies a stable read-only link and revokes it without exposing editing controls', async () => {
|
||||
const alice = makeUser('alice', 'user');
|
||||
const { user, router, session, api } = await renderApp([alice], '/', { signedInAs: alice });
|
||||
const created = await session.client.POST('/api/v1/me/playlists', { body: { name: 'Общий микс', description: 'Для друзей', track_ids: [albumTracks[0]!.id] } });
|
||||
const playlist = created.data!;
|
||||
await router.navigate({ to: '/playlist/$id', params: { id: playlist.id } });
|
||||
await screen.findByRole('heading', { name: 'Общий микс' });
|
||||
await user.click(screen.getByRole('button', { name: 'Поделиться плейлистом' }));
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('Ссылка на плейлист скопирована');
|
||||
const shareId = api.playlists.get(playlist.id)!.share_id!;
|
||||
expect(await navigator.clipboard.readText()).toBe(`${window.location.origin}/shared/playlist/${shareId}`);
|
||||
await user.click(screen.getByRole('button', { name: 'Поделиться плейлистом' }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Поделиться плейлистом' })).toBeEnabled());
|
||||
expect(api.calls.filter(c => c.method === 'POST' && c.path.endsWith('/share'))).toHaveLength(2);
|
||||
expect(api.playlists.get(playlist.id)?.share_id).toBe(shareId);
|
||||
await router.navigate({ to: '/shared/playlist/$shareId', params: { shareId } });
|
||||
await screen.findByRole('heading', { name: 'Общий микс' });
|
||||
expect(screen.queryByRole('button', { name: 'Изменить плейлист' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Управлять треками' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Удалить плейлист' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Пауза' })).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: 'Слушать всё' }));
|
||||
await screen.findByRole('contentinfo', { name: 'Плеер' });
|
||||
await router.navigate({ to: '/playlist/$id', params: { id: playlist.id } });
|
||||
await user.click(await screen.findByRole('button', { name: 'Отключить доступ по ссылке' }));
|
||||
await waitFor(() => expect(api.playlists.get(playlist.id)?.share_id).toBeNull());
|
||||
await user.click(screen.getByRole('button', { name: 'Поделиться плейлистом' }));
|
||||
await waitFor(() => expect(api.playlists.get(playlist.id)?.share_id).toBeTruthy());
|
||||
expect(api.playlists.get(playlist.id)?.share_id).not.toBe(shareId);
|
||||
await router.navigate({ to: '/shared/playlist/$shareId', params: { shareId } });
|
||||
await screen.findByRole('alert');
|
||||
expect(screen.queryByRole('heading', { name: 'Общий микс' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns to the shared playlist after login without starting playback', async () => {
|
||||
const bob = makeUser('bob', 'user');
|
||||
const shareId = '00000000-0000-4000-9001-000000000001';
|
||||
const { api, user, router } = await renderApp([bob], `/shared/playlist/${shareId}`);
|
||||
api.playlists.set('00000000-0000-4000-9000-000000000001', { id: '00000000-0000-4000-9000-000000000001', name: 'От друга', description: '', revision: 1, tracks: [albumTracks[0]!], track_count: 1, duration_ms: 200000, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', share_id: shareId });
|
||||
await screen.findByRole('heading', { name: 'Вход' });
|
||||
await user.type(screen.getByLabelText('Логин'), bob.username);
|
||||
await user.type(screen.getByLabelText('Пароль'), bob.password);
|
||||
await user.click(screen.getByRole('button', { name: 'Войти' }));
|
||||
await screen.findByRole('heading', { name: 'От друга' });
|
||||
expect(router.state.location.pathname).toBe(`/shared/playlist/${shareId}`);
|
||||
expect(screen.queryByRole('button', { name: 'Пауза' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Изменить плейлист' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user