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 (
-
+
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 "新播放清單"