release / govulncheck (push) Successful in 34s
release / web (push) Failing after 1m20s
release / go (push) Successful in 1m37s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 4m38s
release / android (push) Canceled after 4m39s
release / Build signed APK (releases and dev) (push) Canceled after 3m49s
Every track list (album, Liked, Search, Search → Tracks, playlist, History) now draws its rows with TrackListRow inside a TrackList box, in place of four row components that had drifted apart: - TrackListRow owns the look: lead column, title over "artist · album", like, radio, add to queue, menu, duration, then a trailing slot. It also owns the stripe, hover, the now-playing band and the unavailable state. The stripe is dropped on now-playing and selected rows, where Tailwind would otherwise let `odd:` win. - TrackRow, PlaylistTrackRow and HistoryRow are wrappers that keep only their behaviour: track number and multi-select; grip, remove and dead-entry states; cover and timestamp. Playlist and History rows gain radio, add and the menu, so every list offers the same actions. - The radio control is Lucide's Radio icon, not the 📻 emoji. - Enter on a control inside a row now belongs to that control. The old TrackRow played the track when Enter was pressed on "add to queue". - TrackList has no fill, so the stripe shows on playlists too. Drag reorder (#5393): neodrag kept the drop offset, so after the list re-rendered the dragged row sat between two slots. reorderDrag, shared by the playlist and queue rows: - snaps the row to whole rows while dragging; - resets it to its slot on drop; - on playlists, gives the touch gesture to the grip only, so the list still scrolls by touch. The playlist page reorders optimistically and rolls back on error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.0 KiB
TypeScript
105 lines
4.0 KiB
TypeScript
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<string, unknown> = {}) {
|
|
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();
|
|
});
|
|
});
|