From ae3e23872a312e2387996ef956ec9b0dd79be4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20de=20la=20Martini=C3=A8re?= Date: Mon, 23 Mar 2026 23:33:01 +0100 Subject: [PATCH] Playlists E2E tests --- src/__tests__/library.test-e2e.tsx | 32 ++++-------- src/__tests__/player.test-e2e.tsx | 8 ++- src/__tests__/playlists.test-e2e.tsx | 77 ++++++++++++++++++++++++++++ src/__tests__/test-helpers.tsx | 28 ++++++++++ src/components/SideNav.tsx | 7 ++- src/components/SideNavLink.tsx | 12 +++++ src/elements/ButtonIcon.tsx | 13 ++++- src/elements/Link.tsx | 19 ++++--- src/lib/__mocks__/bridge-database.ts | 49 ++++++++++++------ src/routes/playlists.tsx | 6 ++- src/translations/en.po | 8 +-- src/translations/es.po | 8 +-- src/translations/fr.po | 8 +-- src/translations/ja.po | 8 +-- src/translations/ru.po | 8 +-- src/translations/zh-CN.po | 8 +-- src/translations/zh-TW.po | 8 +-- 17 files changed, 226 insertions(+), 81 deletions(-) create mode 100644 src/__tests__/playlists.test-e2e.tsx diff --git a/src/__tests__/library.test-e2e.tsx b/src/__tests__/library.test-e2e.tsx index bee2873ea..c1252c192 100644 --- a/src/__tests__/library.test-e2e.tsx +++ b/src/__tests__/library.test-e2e.tsx @@ -1,15 +1,13 @@ import { expect, test } from 'vite-plus/test'; import { page, userEvent } from 'vite-plus/test/browser'; -import { beforeEachSetup } from './test-helpers'; +import { beforeEachSetup, goToLibrary, scanLibrary } from './test-helpers'; beforeEachSetup(); test('The library tab should display all tracks', async () => { - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); // Ensure we have the 3 test-tracks, but no more await expect.element(page.getByTestId('track-row-0')).toBeInTheDocument(); @@ -29,10 +27,8 @@ test('The library tab should display all tracks', async () => { }); test('Tracks should selectable via click + modifiers', async () => { - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); const firstTrack = page.getByTestId(/track-row-/).first(); const secondTrack = page.getByTestId(/track-row-/).nth(1); @@ -72,10 +68,8 @@ test('Tracks should selectable via click + modifiers', async () => { }); test('Tracks should be selectable via keyboard only (after a single selection)', async () => { - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); const firstTrack = page.getByTestId(/track-row-/).first(); const secondTrack = page.getByTestId(/track-row-/).nth(1); @@ -131,10 +125,8 @@ test('Tracks should be selectable via keyboard only (after a single selection)', }); test('Search should filter tracks in the library', async () => { - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); const search = page.getByTestId('library-search'); const searchClear = page.getByTestId('library-search-clear'); @@ -174,10 +166,8 @@ test('Search should filter tracks in the library', async () => { }); test('Column headers should sort tracks in the library', async () => { - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); const firstTrack = page.getByTestId(/track-row-/).first(); const secondTrack = page.getByTestId(/track-row-/).nth(1); diff --git a/src/__tests__/player.test-e2e.tsx b/src/__tests__/player.test-e2e.tsx index 27ea4acd4..add1de3a7 100644 --- a/src/__tests__/player.test-e2e.tsx +++ b/src/__tests__/player.test-e2e.tsx @@ -1,7 +1,7 @@ import { expect, test } from 'vite-plus/test'; import { page } from 'vite-plus/test/browser'; -import { beforeEachSetup } from './test-helpers'; +import { beforeEachSetup, goToLibrary, scanLibrary } from './test-helpers'; beforeEachSetup(); @@ -14,10 +14,8 @@ test('Double click on a track should play it and display its metadata', async () .element(page.getByTestId('playercontrol-pause')) .not.toBeInTheDocument(); - // Fake the import of tracks - await page.getByTestId('footer-settings-link').click(); - await page.getByTestId('scan-library-button').click(); - await page.getByTestId('footer-library-link').click(); + await scanLibrary(); + await goToLibrary(); // Double-clicking on a track should start the player await page.getByTestId('track-row-0').dblClick(); diff --git a/src/__tests__/playlists.test-e2e.tsx b/src/__tests__/playlists.test-e2e.tsx new file mode 100644 index 000000000..9261905ac --- /dev/null +++ b/src/__tests__/playlists.test-e2e.tsx @@ -0,0 +1,77 @@ +import { expect, test } from 'vite-plus/test'; +import { page, userEvent } from 'vite-plus/test/browser'; + +import { beforeEachSetup, goToPlaylists } from './test-helpers'; + +beforeEachSetup(); + +async function createPlaylist() { + await page.getByTestId('playlist-new-button').click(); +} + +async function renamePlaylist(currentName: string, newName: string) { + await page.getByRole('link', { name: currentName }).dblClick(); + const input = page.getByTestId('playlist-rename-input'); + await input.clear(); + await input.fill(newName); + await userEvent.keyboard('[Enter]'); +} + +test('Playlists', async () => { + await goToPlaylists(); + + // Empty state when no playlists exist + await expect + .element(page.getByTestId('view-message')) + .toHaveTextContent("You haven't created any playlist yet"); + + // Clicking "create one now" creates a playlist and shows the empty playlist view + await page.getByTestId('create-playlist-call-to-action').click(); + await expect + .element(page.getByTestId('view-message')) + .toHaveTextContent('Empty playlist'); + + // Creating more playlists via the + button shows them all in the sidebar + await createPlaylist(); + await createPlaylist(); + const playlistLinks = page.getByRole('link', { name: 'New playlist' }); + await expect.element(playlistLinks.nth(0)).toBeInTheDocument(); + await expect.element(playlistLinks.nth(1)).toBeInTheDocument(); + await expect.element(playlistLinks.nth(2)).toBeInTheDocument(); + await expect.element(playlistLinks.nth(3)).not.toBeInTheDocument(); + + // Rename via Escape cancels the rename + await page.getByRole('link', { name: 'New playlist' }).first().dblClick(); + const input = page.getByTestId('playlist-rename-input'); + await input.clear(); + await input.fill('Cancelled Name'); + await userEvent.keyboard('[Escape]'); + await expect + .element(page.getByRole('link', { name: 'Cancelled Name' })) + .not.toBeInTheDocument(); + + // Rename via Enter commits the new name + await renamePlaylist('New playlist', 'Alpha'); + await expect + .element(page.getByRole('link', { name: 'Alpha' })) + .toBeInTheDocument(); + + await renamePlaylist('New playlist', 'Another Blues'); + await renamePlaylist('New playlist', 'Best Of'); + + // Playlists are grouped by first letter + const letterGroups = page.getByTestId('sidenav-letter-group'); + await expect.element(letterGroups.nth(0)).toHaveTextContent('A'); + await expect.element(letterGroups.nth(1)).toHaveTextContent('B'); + await expect.element(letterGroups.nth(2)).not.toBeInTheDocument(); + + await expect + .element(page.getByRole('link', { name: 'Alpha' })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('link', { name: 'Another Blues' })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('link', { name: 'Best Of' })) + .toBeInTheDocument(); +}); diff --git a/src/__tests__/test-helpers.tsx b/src/__tests__/test-helpers.tsx index 8f4b960d1..6293e5963 100644 --- a/src/__tests__/test-helpers.tsx +++ b/src/__tests__/test-helpers.tsx @@ -1,5 +1,33 @@ import { i18n } from '@lingui/core'; import { beforeEach, vi } from 'vite-plus/test'; +import { page } from 'vite-plus/test/browser'; + +// --------------------------------------------------------------------------- +// Navigation helpers +// --------------------------------------------------------------------------- + +export async function goToLibrary() { + await page.getByTestId('footer-library-link').click(); +} + +export async function goToPlaylists() { + await page.getByTestId('footer-playlists-link').click(); +} + +export async function goToSettings() { + await page.getByTestId('footer-settings-link').click(); +} + +// --------------------------------------------------------------------------- +// Library helpers +// --------------------------------------------------------------------------- + +/** Triggers a library scan using the mock tracks */ +export async function scanLibrary() { + await goToSettings(); + await page.getByTestId('scan-library-button').click(); +} + import { render } from 'vitest-browser-react'; import { MOCK_CONFIG } from '../lib/__mocks__/bridge-config.ts'; diff --git a/src/components/SideNav.tsx b/src/components/SideNav.tsx index 284effcaf..4cbb828e5 100644 --- a/src/components/SideNav.tsx +++ b/src/components/SideNav.tsx @@ -39,7 +39,12 @@ export default function SideNav(props: Props) { {Object.entries(groupedChildren).map(([letter, children]) => { return ( -
{letter}
+
+ {letter} +
{children}
); diff --git a/src/components/SideNavLink.tsx b/src/components/SideNavLink.tsx index ea5c18143..72ca02aa7 100644 --- a/src/components/SideNavLink.tsx +++ b/src/components/SideNavLink.tsx @@ -126,11 +126,22 @@ export default function SideNavLink(props: Props): React.ReactNode { onBlur={onBlur} onFocus={onFocus} ref={(ref) => ref?.focus()} + data-testid="playlist-rename-input" {...stylex.props(styles.sideNavLink, styles.sideNavLinkInput)} /> ); } + const onDoubleClick = useCallback( + (e: React.MouseEvent) => { + if (onRename) { + e.preventDefault(); + setRenamed(true); + } + }, + [onRename], + ); + return ( & { iconSize?: IconSize; isActive?: boolean; xstyle?: stylex.CompiledStyles; + 'data-testid'?: string; }; export default function ButtonIcon(props: Props) { - const { onClick, icon, iconSize, isActive, ref, xstyle, ...rest } = props; + const { + onClick, + icon, + iconSize, + isActive, + ref, + xstyle, + 'data-testid': testId, + ...rest + } = props; return ( ); diff --git a/src/lib/__mocks__/bridge-database.ts b/src/lib/__mocks__/bridge-database.ts index 15e593a7e..a39839134 100644 --- a/src/lib/__mocks__/bridge-database.ts +++ b/src/lib/__mocks__/bridge-database.ts @@ -61,6 +61,9 @@ const MOCK_TRACKS: Array = [ // Weirdly, when using a class property, accessing it is extremely slow. No idea why. May be a webkit issue. let tracks: Array = []; +let playlists: Array = []; +let nextPlaylistId = 0; + class DatabaseBridge implements DatabaseBridgeInterface { async getAllTracks(): Promise> { return tracks; @@ -83,6 +86,8 @@ class DatabaseBridge implements DatabaseBridgeInterface { _refresh = false, ): Promise { tracks = MOCK_TRACKS; + playlists = []; + nextPlaylistId = 0; return { playlist_count: 0, @@ -109,39 +114,49 @@ class DatabaseBridge implements DatabaseBridgeInterface { } async getAllPlaylists(): Promise> { - return []; + return playlists; } - async getPlaylist(_id: string): Promise { - return { - id: '0', - name: 'test playlist', - tracks: [], - import_path: null, - }; + async getPlaylist(id: string): Promise { + const playlist = playlists.find((p) => p.id === id); + if (!playlist) throw 'Playlist not found'; + return playlist; } - async createPlaylist(_name: string, _ids: Array): Promise { - return this.getPlaylist('0'); + async createPlaylist(name: string, ids: Array): Promise { + const playlist: Playlist = { + id: String(nextPlaylistId++), + name, + tracks: ids, + import_path: null, + }; + playlists.push(playlist); + return playlist; } - async renamePlaylist(_id: string, _name: string): Promise { - return this.getPlaylist('0'); + async renamePlaylist(id: string, name: string): Promise { + const playlist = playlists.find((p) => p.id === id); + if (!playlist) throw 'Playlist not found'; + playlist.name = name; + return playlist; } async setPlaylistTracks( - _id: string, - _tracks: Array, + id: string, + trackIDs: Array, ): Promise { - return this.getPlaylist('0'); + const playlist = playlists.find((p) => p.id === id); + if (!playlist) throw 'Playlist not found'; + playlist.tracks = trackIDs; + return playlist; } async exportPlaylist(_id: string): Promise { return; } - async deletePlaylist(_id: string): Promise { - return; + async deletePlaylist(id: string): Promise { + playlists = playlists.filter((p) => p.id !== id); } async reset(): Promise { diff --git a/src/routes/playlists.tsx b/src/routes/playlists.tsx index d8e370159..4ae4e91b2 100644 --- a/src/routes/playlists.tsx +++ b/src/routes/playlists.tsx @@ -140,7 +140,10 @@ function ViewPlaylists() { You haven{"'"}t created any playlist yet

- + create one now @@ -168,6 +171,7 @@ function ViewPlaylists() { icon="plus" onClick={createPlaylist} title={t`New Playlist`} + data-testid="playlist-new-button" /> } > diff --git a/src/translations/en.po b/src/translations/en.po index 2f56d89d1..97cc23531 100644 --- a/src/translations/en.po +++ b/src/translations/en.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "Artists" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "Playlists" @@ -315,15 +315,15 @@ msgstr "Export" msgid "You haven't created any playlist yet" msgstr "You haven't created any playlist yet" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "create one now" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "No playlist selected" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "New Playlist" diff --git a/src/translations/es.po b/src/translations/es.po index e43476e2e..aff2886b1 100644 --- a/src/translations/es.po +++ b/src/translations/es.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "Artistas" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "Listas de reproducción" @@ -315,15 +315,15 @@ msgstr "Exportar" msgid "You haven't created any playlist yet" msgstr "No has creado ninguna lista de reproducción todavía" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "crea una ahora" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "Ninguna lista seleccionada" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "Nueva Lista" diff --git a/src/translations/fr.po b/src/translations/fr.po index e1790a837..2c6923270 100644 --- a/src/translations/fr.po +++ b/src/translations/fr.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "Artistes" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "Playlists" @@ -315,15 +315,15 @@ msgstr "Exporter" msgid "You haven't created any playlist yet" msgstr "Vous n'avez pas encore créé de playlist" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "créez-en une maintenant" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "Aucune playlist sélectionnée" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "Nouvelle Playlist" diff --git a/src/translations/ja.po b/src/translations/ja.po index d67d07d39..e0ac820b5 100644 --- a/src/translations/ja.po +++ b/src/translations/ja.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "アーティスト" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "プレイリスト" @@ -315,15 +315,15 @@ msgstr "エクスポート" msgid "You haven't created any playlist yet" msgstr "まだプレイリストを作成していません" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "今すぐ作成" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "プレイリストが選択されていません" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "新しいプレイリスト" diff --git a/src/translations/ru.po b/src/translations/ru.po index 2253f171d..261725d9d 100644 --- a/src/translations/ru.po +++ b/src/translations/ru.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "Исполнители" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "Плейлисты" @@ -315,15 +315,15 @@ msgstr "Экспортировать" msgid "You haven't created any playlist yet" msgstr "Вы пока не создали ни одного плейлиста" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "создать сейчас" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "Плейлист не выбран" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "Новый плейлист" diff --git a/src/translations/zh-CN.po b/src/translations/zh-CN.po index 57f3176dc..1f412c206 100644 --- a/src/translations/zh-CN.po +++ b/src/translations/zh-CN.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "艺术家" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "播放列表" @@ -315,15 +315,15 @@ msgstr "导出" msgid "You haven't created any playlist yet" msgstr "您还没有创建任何播放列表" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "现在创建一个" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "未选择播放列表" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "新播放列表" diff --git a/src/translations/zh-TW.po b/src/translations/zh-TW.po index 7ab09bee2..48b12f1f8 100644 --- a/src/translations/zh-TW.po +++ b/src/translations/zh-TW.po @@ -87,7 +87,7 @@ msgid "Artists" msgstr "藝術家" #: src/components/Navigation.tsx:68 -#: src/routes/playlists.tsx:165 +#: src/routes/playlists.tsx:168 #: src/routes/settings.ui.tsx:150 msgid "Playlists" msgstr "播放清單" @@ -315,15 +315,15 @@ msgstr "匯出" msgid "You haven't created any playlist yet" msgstr "您還沒有建立任何播放清單" -#: src/routes/playlists.tsx:144 +#: src/routes/playlists.tsx:147 msgid "create one now" msgstr "現在建立一個" -#: src/routes/playlists.tsx:153 +#: src/routes/playlists.tsx:156 msgid "No playlist selected" msgstr "未選擇播放清單" -#: src/routes/playlists.tsx:170 +#: src/routes/playlists.tsx:173 msgid "New Playlist" msgstr "新播放清單"