import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import { emptyLikesMock } from '../../test-utils/mocks/likes'; import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine'; import { makeTrack } from '#test-utils/fixtures/track.js'; const player = vi.hoisted(() => ({ current: undefined as { id: string } | undefined })); vi.mock('#lib/player/store.svelte.js', () => ({ playNext: vi.fn(), enqueueTrack: vi.fn(), playQueue: vi.fn(), playRadio: vi.fn(), player })); vi.mock('#lib/api/likes.js', () => emptyLikesMock()); vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock()); import TrackListRow from './TrackListRow.svelte'; import { enqueueTrack, playRadio } from '#lib/player/store.svelte.js'; const track = makeTrack({ id: 't1', title: 'Roygbiv', duration_sec: 137 }); function base(overrides: Record = {}) { return { title: 'Roygbiv', subtitle: 'Boards of Canada · MHTRTC', durationSec: 137, track, ariaLabel: 'Roygbiv', onActivate: vi.fn(), ...overrides }; } afterEach(() => { vi.clearAllMocks(); player.current = undefined; }); describe('TrackListRow', () => { test('draws title, subtitle and duration', () => { render(TrackListRow, { props: base() }); expect(screen.getByText('Roygbiv')).toBeInTheDocument(); expect(screen.getByText('Boards of Canada · MHTRTC')).toBeInTheDocument(); expect(screen.getByText('2:17')).toBeInTheDocument(); }); test('a plain row is striped; the now-playing row is banded instead', () => { const { unmount } = render(TrackListRow, { props: base() }); expect(screen.getByRole('button', { name: 'Roygbiv' }).className).toContain('odd:bg-surface/50'); unmount(); player.current = { id: 't1' }; render(TrackListRow, { props: base() }); const row = screen.getByRole('button', { name: 'Roygbiv' }); expect(row.className).toContain('border-l-accent'); expect(row.className).not.toContain('odd:bg-surface/50'); }); test('click and Enter activate the row', async () => { const props = base(); render(TrackListRow, { props }); const row = screen.getByRole('button', { name: 'Roygbiv' }); await fireEvent.click(row); await fireEvent.keyDown(row, { key: 'Enter' }); expect(props.onActivate).toHaveBeenCalledTimes(2); }); // Enter on a control inside the row is that control's, not the row's. // The old TrackRow played the track when Enter was pressed on "add". test('Enter on an inner control does not activate the row', async () => { const props = base(); render(TrackListRow, { props }); await fireEvent.keyDown(screen.getByRole('button', { name: /add roygbiv to queue/i }), { key: 'Enter' }); expect(props.onActivate).not.toHaveBeenCalled(); }); test('radio and add act on the track without activating the row', async () => { const props = base(); render(TrackListRow, { props }); await fireEvent.click(screen.getByRole('button', { name: /play radio/i })); await fireEvent.click(screen.getByRole('button', { name: /add roygbiv to queue/i })); expect(playRadio).toHaveBeenCalledWith('t1'); expect(enqueueTrack).toHaveBeenCalledWith(track); expect(props.onActivate).not.toHaveBeenCalled(); }); test('a row with no playable track hides like, radio, add and the menu', () => { render(TrackListRow, { props: base({ track: null, disabled: true }) }); expect(screen.queryByRole('button', { name: /play radio/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /to queue/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /track actions for/i })).not.toBeInTheDocument(); }); test('a disabled row does not activate', async () => { const props = base({ disabled: true }); render(TrackListRow, { props }); await fireEvent.click(screen.getByRole('button', { name: 'Roygbiv' })); expect(props.onActivate).not.toHaveBeenCalled(); }); });