Skip to content

Commit d70c8db

Browse files
committed
Playlists E2E tests
1 parent 196a96c commit d70c8db

16 files changed

Lines changed: 214 additions & 74 deletions

File tree

src/__tests__/library.test-e2e.tsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { expect, test } from 'vite-plus/test';
22
import { page, userEvent } from 'vite-plus/test/browser';
33

4-
import { beforeEachSetup } from './test-helpers';
4+
import { beforeEachSetup, goToLibrary, scanLibrary } from './test-helpers';
55

66
beforeEachSetup();
77

88
test('The library tab should display all tracks', async () => {
9-
// Fake the import of tracks
10-
await page.getByTestId('footer-settings-link').click();
11-
await page.getByTestId('scan-library-button').click();
12-
await page.getByTestId('footer-library-link').click();
9+
await scanLibrary();
10+
await goToLibrary();
1311

1412
// Ensure we have the 3 test-tracks, but no more
1513
await expect.element(page.getByTestId('track-row-0')).toBeInTheDocument();
@@ -29,10 +27,8 @@ test('The library tab should display all tracks', async () => {
2927
});
3028

3129
test('Tracks should selectable via click + modifiers', async () => {
32-
// Fake the import of tracks
33-
await page.getByTestId('footer-settings-link').click();
34-
await page.getByTestId('scan-library-button').click();
35-
await page.getByTestId('footer-library-link').click();
30+
await scanLibrary();
31+
await goToLibrary();
3632

3733
const firstTrack = page.getByTestId(/track-row-/).first();
3834
const secondTrack = page.getByTestId(/track-row-/).nth(1);
@@ -72,10 +68,8 @@ test('Tracks should selectable via click + modifiers', async () => {
7268
});
7369

7470
test('Tracks should be selectable via keyboard only (after a single selection)', async () => {
75-
// Fake the import of tracks
76-
await page.getByTestId('footer-settings-link').click();
77-
await page.getByTestId('scan-library-button').click();
78-
await page.getByTestId('footer-library-link').click();
71+
await scanLibrary();
72+
await goToLibrary();
7973

8074
const firstTrack = page.getByTestId(/track-row-/).first();
8175
const secondTrack = page.getByTestId(/track-row-/).nth(1);
@@ -131,10 +125,8 @@ test('Tracks should be selectable via keyboard only (after a single selection)',
131125
});
132126

133127
test('Search should filter tracks in the library', async () => {
134-
// Fake the import of tracks
135-
await page.getByTestId('footer-settings-link').click();
136-
await page.getByTestId('scan-library-button').click();
137-
await page.getByTestId('footer-library-link').click();
128+
await scanLibrary();
129+
await goToLibrary();
138130

139131
const search = page.getByTestId('library-search');
140132
const searchClear = page.getByTestId('library-search-clear');
@@ -174,10 +166,8 @@ test('Search should filter tracks in the library', async () => {
174166
});
175167

176168
test('Column headers should sort tracks in the library', async () => {
177-
// Fake the import of tracks
178-
await page.getByTestId('footer-settings-link').click();
179-
await page.getByTestId('scan-library-button').click();
180-
await page.getByTestId('footer-library-link').click();
169+
await scanLibrary();
170+
await goToLibrary();
181171

182172
const firstTrack = page.getByTestId(/track-row-/).first();
183173
const secondTrack = page.getByTestId(/track-row-/).nth(1);

src/__tests__/player.test-e2e.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, test } from 'vite-plus/test';
22
import { page } from 'vite-plus/test/browser';
33

4-
import { beforeEachSetup } from './test-helpers';
4+
import { beforeEachSetup, goToLibrary, scanLibrary } from './test-helpers';
55

66
beforeEachSetup();
77

@@ -14,10 +14,8 @@ test('Double click on a track should play it and display its metadata', async ()
1414
.element(page.getByTestId('playercontrol-pause'))
1515
.not.toBeInTheDocument();
1616

17-
// Fake the import of tracks
18-
await page.getByTestId('footer-settings-link').click();
19-
await page.getByTestId('scan-library-button').click();
20-
await page.getByTestId('footer-library-link').click();
17+
await scanLibrary();
18+
await goToLibrary();
2119

2220
// Double-clicking on a track should start the player
2321
await page.getByTestId('track-row-0').dblClick();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect, test } from 'vite-plus/test';
2+
import { page, userEvent } from 'vite-plus/test/browser';
3+
4+
import { beforeEachSetup, goToPlaylists } from './test-helpers';
5+
6+
beforeEachSetup();
7+
8+
async function createPlaylist() {
9+
await page.getByTestId('playlist-new-button').click();
10+
}
11+
12+
async function renamePlaylist(currentName: string, newName: string) {
13+
await page.getByRole('link', { name: currentName }).dblClick();
14+
const input = page.getByTestId('playlist-rename-input');
15+
await input.clear();
16+
await input.fill(newName);
17+
await userEvent.keyboard('[Enter]');
18+
}
19+
20+
test('Playlists', async () => {
21+
await goToPlaylists();
22+
23+
// Empty state when no playlists exist
24+
await expect
25+
.element(page.getByTestId('view-message'))
26+
.toHaveTextContent("You haven't created any playlist yet");
27+
28+
// Clicking "create one now" creates a playlist and shows the empty playlist view
29+
await page.getByTestId('create-playlist-call-to-action').click();
30+
await expect
31+
.element(page.getByTestId('view-message'))
32+
.toHaveTextContent('Empty playlist');
33+
34+
// Creating more playlists via the + button shows them all in the sidebar
35+
await createPlaylist();
36+
await createPlaylist();
37+
const playlistLinks = page.getByRole('link', { name: 'New playlist' });
38+
await expect.element(playlistLinks.nth(0)).toBeInTheDocument();
39+
await expect.element(playlistLinks.nth(1)).toBeInTheDocument();
40+
await expect.element(playlistLinks.nth(2)).toBeInTheDocument();
41+
await expect.element(playlistLinks.nth(3)).not.toBeInTheDocument();
42+
43+
// Rename via Escape cancels the rename
44+
await page.getByRole('link', { name: 'New playlist' }).first().dblClick();
45+
const input = page.getByTestId('playlist-rename-input');
46+
await input.clear();
47+
await input.fill('Cancelled Name');
48+
await userEvent.keyboard('[Escape]');
49+
await expect
50+
.element(page.getByRole('link', { name: 'Cancelled Name' }))
51+
.not.toBeInTheDocument();
52+
53+
// Rename via Enter commits the new name
54+
await renamePlaylist('New playlist', 'Alpha');
55+
await expect
56+
.element(page.getByRole('link', { name: 'Alpha' }))
57+
.toBeInTheDocument();
58+
59+
await renamePlaylist('New playlist', 'Another Blues');
60+
await renamePlaylist('New playlist', 'Best Of');
61+
62+
// Playlists are grouped by first letter
63+
const letterGroups = page.getByTestId('sidenav-letter-group');
64+
await expect.element(letterGroups.nth(0)).toHaveTextContent('A');
65+
await expect.element(letterGroups.nth(1)).toHaveTextContent('B');
66+
await expect.element(letterGroups.nth(2)).not.toBeInTheDocument();
67+
68+
await expect
69+
.element(page.getByRole('link', { name: 'Alpha' }))
70+
.toBeInTheDocument();
71+
await expect
72+
.element(page.getByRole('link', { name: 'Another Blues' }))
73+
.toBeInTheDocument();
74+
await expect
75+
.element(page.getByRole('link', { name: 'Best Of' }))
76+
.toBeInTheDocument();
77+
});

src/__tests__/test-helpers.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
import { i18n } from '@lingui/core';
22
import { beforeEach, vi } from 'vite-plus/test';
3+
import { page } from 'vite-plus/test/browser';
4+
5+
// ---------------------------------------------------------------------------
6+
// Navigation helpers
7+
// ---------------------------------------------------------------------------
8+
9+
export async function goToLibrary() {
10+
await page.getByTestId('footer-library-link').click();
11+
}
12+
13+
export async function goToPlaylists() {
14+
await page.getByTestId('footer-playlists-link').click();
15+
}
16+
17+
export async function goToSettings() {
18+
await page.getByTestId('footer-settings-link').click();
19+
}
20+
21+
// ---------------------------------------------------------------------------
22+
// Library helpers
23+
// ---------------------------------------------------------------------------
24+
25+
/** Triggers a library scan using the mock tracks */
26+
export async function scanLibrary() {
27+
await goToSettings();
28+
await page.getByTestId('scan-library-button').click();
29+
}
30+
331
import { render } from 'vitest-browser-react';
432

533
import { MOCK_CONFIG } from '../lib/__mocks__/bridge-config.ts';

src/components/SideNav.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export default function SideNav(props: Props) {
3939
{Object.entries(groupedChildren).map(([letter, children]) => {
4040
return (
4141
<React.Fragment key={letter}>
42-
<div {...stylex.props(styles.letter)}>{letter}</div>
42+
<div
43+
{...stylex.props(styles.letter)}
44+
data-testid="sidenav-letter-group"
45+
>
46+
{letter}
47+
</div>
4348
<div {...stylex.props(styles.items)}>{children}</div>
4449
</React.Fragment>
4550
);

src/components/SideNavLink.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,30 @@ export default function SideNavLink(props: Props): React.ReactNode {
126126
onBlur={onBlur}
127127
onFocus={onFocus}
128128
ref={(ref) => ref?.focus()}
129+
data-testid="playlist-rename-input"
129130
{...stylex.props(styles.sideNavLink, styles.sideNavLinkInput)}
130131
/>
131132
);
132133
}
133134

135+
const onDoubleClick = useCallback(
136+
(e: React.MouseEvent) => {
137+
if (onRename) {
138+
e.preventDefault();
139+
setRenamed(true);
140+
}
141+
},
142+
[onRename],
143+
);
144+
134145
return (
135146
<NavigationMenu.Item>
136147
<NavigationMenu.Link
137148
render={(renderProps) => (
138149
<Link
139150
{...renderProps}
140151
onContextMenu={onContextMenu}
152+
onDoubleClick={onDoubleClick}
141153
draggable={false}
142154
{...linkOptions}
143155
{...stylex.props(styles.sideNavLink)}

src/elements/ButtonIcon.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,27 @@ type Props = React.ComponentPropsWithRef<'button'> & {
88
iconSize?: IconSize;
99
isActive?: boolean;
1010
xstyle?: stylex.CompiledStyles;
11+
'data-testid'?: string;
1112
};
1213

1314
export default function ButtonIcon(props: Props) {
14-
const { onClick, icon, iconSize, isActive, ref, xstyle, ...rest } = props;
15+
const {
16+
onClick,
17+
icon,
18+
iconSize,
19+
isActive,
20+
ref,
21+
xstyle,
22+
'data-testid': testId,
23+
...rest
24+
} = props;
1525
return (
1626
<button
1727
ref={ref}
1828
type="button"
1929
onClick={onClick}
2030
data-museeks-action
31+
data-testid={testId}
2132
{...rest}
2233
{...stylex.props(styles.button, xstyle)}
2334
>

src/lib/__mocks__/bridge-database.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ const MOCK_TRACKS: Array<Track> = [
6161
// Weirdly, when using a class property, accessing it is extremely slow. No idea why. May be a webkit issue.
6262
let tracks: Array<Track> = [];
6363

64+
let playlists: Array<Playlist> = [];
65+
let nextPlaylistId = 0;
66+
6467
class DatabaseBridge implements DatabaseBridgeInterface {
6568
async getAllTracks(): Promise<Array<Track>> {
6669
return tracks;
@@ -83,6 +86,8 @@ class DatabaseBridge implements DatabaseBridgeInterface {
8386
_refresh = false,
8487
): Promise<ScanResult> {
8588
tracks = MOCK_TRACKS;
89+
playlists = [];
90+
nextPlaylistId = 0;
8691

8792
return {
8893
playlist_count: 0,
@@ -109,39 +114,49 @@ class DatabaseBridge implements DatabaseBridgeInterface {
109114
}
110115

111116
async getAllPlaylists(): Promise<Array<Playlist>> {
112-
return [];
117+
return playlists;
113118
}
114119

115-
async getPlaylist(_id: string): Promise<Playlist> {
116-
return {
117-
id: '0',
118-
name: 'test playlist',
119-
tracks: [],
120-
import_path: null,
121-
};
120+
async getPlaylist(id: string): Promise<Playlist> {
121+
const playlist = playlists.find((p) => p.id === id);
122+
if (!playlist) throw 'Playlist not found';
123+
return playlist;
122124
}
123125

124-
async createPlaylist(_name: string, _ids: Array<string>): Promise<Playlist> {
125-
return this.getPlaylist('0');
126+
async createPlaylist(name: string, ids: Array<string>): Promise<Playlist> {
127+
const playlist: Playlist = {
128+
id: String(nextPlaylistId++),
129+
name,
130+
tracks: ids,
131+
import_path: null,
132+
};
133+
playlists.push(playlist);
134+
return playlist;
126135
}
127136

128-
async renamePlaylist(_id: string, _name: string): Promise<Playlist> {
129-
return this.getPlaylist('0');
137+
async renamePlaylist(id: string, name: string): Promise<Playlist> {
138+
const playlist = playlists.find((p) => p.id === id);
139+
if (!playlist) throw 'Playlist not found';
140+
playlist.name = name;
141+
return playlist;
130142
}
131143

132144
async setPlaylistTracks(
133-
_id: string,
134-
_tracks: Array<string>,
145+
id: string,
146+
trackIDs: Array<string>,
135147
): Promise<Playlist> {
136-
return this.getPlaylist('0');
148+
const playlist = playlists.find((p) => p.id === id);
149+
if (!playlist) throw 'Playlist not found';
150+
playlist.tracks = trackIDs;
151+
return playlist;
137152
}
138153

139154
async exportPlaylist(_id: string): Promise<void> {
140155
return;
141156
}
142157

143-
async deletePlaylist(_id: string): Promise<void> {
144-
return;
158+
async deletePlaylist(id: string): Promise<void> {
159+
playlists = playlists.filter((p) => p.id !== id);
145160
}
146161

147162
async reset(): Promise<string | null> {

src/routes/playlists.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ function ViewPlaylists() {
140140
<Trans>You haven{"'"}t created any playlist yet</Trans>
141141
</p>
142142
<ViewMessage.Sub>
143-
<Link onClick={createPlaylist}>
143+
<Link
144+
onClick={createPlaylist}
145+
data-testid="create-playlist-call-to-action"
146+
>
144147
<Trans>create one now</Trans>
145148
</Link>
146149
</ViewMessage.Sub>
@@ -168,6 +171,7 @@ function ViewPlaylists() {
168171
icon="plus"
169172
onClick={createPlaylist}
170173
title={t`New Playlist`}
174+
data-testid="playlist-new-button"
171175
/>
172176
}
173177
>

0 commit comments

Comments
 (0)