Files
minstrel/web/src/lib/components/TrackMenu.test.ts
T
bvandeusen 9a166f9032
test-web / test (push) Successful in 32s
test(web): update TrackMenu spec for Android-parity kebab
The kebab gained "Start radio" and dropped the duplicate "Flag this track…"
(its action now lives solely under "Hide", which opens the same FlagPopover).
Net item count is unchanged (9 admin / 8 non-admin), but the named-item and
flag-entry assertions needed updating:
- mock playRadio in the store mock; assert Start radio dispatches playRadio.
- swap the flag-item presence check for start-radio.
- replace the "click Flag" test with "click Hide opens the popover".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:48:57 -04:00

134 lines
5.7 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
// Mutable mock-user handle so individual tests can flip is_admin / null
// without re-importing the module under test.
const userState = vi.hoisted(() => ({
current: { id: 'u1', username: 'admin', is_admin: true } as
| { id: string; username: string; is_admin: boolean }
| null
}));
vi.mock('$lib/auth/store.svelte', () => ({
user: { get value() { return userState.current; } }
}));
vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('$lib/api/admin/tracks', () => ({
removeTrack: vi.fn().mockResolvedValue({ deleted_track_id: 't1' })
}));
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn()
}));
import TrackMenu from './TrackMenu.svelte';
import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte';
const track = makeTrack({ title: 'Roygbiv' });
afterEach(() => {
vi.clearAllMocks();
userState.current = { id: 'u1', username: 'admin', is_admin: true };
});
describe('TrackMenu', () => {
test('kebab toggles aria-expanded', async () => {
render(TrackMenu, { props: { track } });
const kebab = screen.getByRole('button', { name: /track actions for roygbiv/i });
expect(kebab.getAttribute('aria-expanded')).toBe('false');
await fireEvent.click(kebab);
expect(kebab.getAttribute('aria-expanded')).toBe('true');
});
test('opening menu shows all 9 entries for an admin user', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
expect(screen.getByRole('menuitem', { name: /play next/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /add to queue/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /^like$/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /add to playlist/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /start radio/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /go to album/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /go to artist/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /^hide$/i })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /remove from library/i })).toBeInTheDocument();
expect(screen.getAllByRole('menuitem')).toHaveLength(9);
});
test('"Remove from library" hidden for non-admin', async () => {
userState.current = { id: 'u2', username: 'normal', is_admin: false };
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
expect(screen.queryByRole('menuitem', { name: /remove from library/i })).not.toBeInTheDocument();
expect(screen.getAllByRole('menuitem')).toHaveLength(8);
});
test('"Add to playlist…" is enabled and opens the AddToPlaylistMenu submenu', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
const item = screen.getByRole('menuitem', { name: /add to playlist/i });
expect(item.getAttribute('aria-disabled')).toBe('false');
await fireEvent.click(item);
await waitFor(() =>
expect(screen.getByRole('menu', { name: /add to playlist/i })).toBeInTheDocument()
);
});
test('"Play next" dispatches playNext(track)', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /play next/i }));
expect(playNext).toHaveBeenCalledWith(track);
});
test('"Add to queue" dispatches enqueueTrack(track)', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /add to queue/i }));
expect(enqueueTrack).toHaveBeenCalledWith(track);
});
test('Escape closes the menu', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
expect(screen.getByRole('menu')).toBeInTheDocument();
await fireEvent.keyDown(window, { key: 'Escape' });
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});
test('"Start radio" dispatches playRadio(track.id)', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /start radio/i }));
expect(playRadio).toHaveBeenCalledWith(track.id);
});
test('clicking "Hide" opens the flag/quarantine popover and closes menu', async () => {
render(TrackMenu, { props: { track } });
await fireEvent.click(screen.getByRole('button', { name: /track actions/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /^hide$/i }));
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
expect(
screen.getByRole('dialog', { name: /flag this track as broken/i })
).toBeInTheDocument();
});
});