diff --git a/web/src/lib/components/PlaylistTrackRow.svelte b/web/src/lib/components/PlaylistTrackRow.svelte
new file mode 100644
index 00000000..8697b5fb
--- /dev/null
+++ b/web/src/lib/components/PlaylistTrackRow.svelte
@@ -0,0 +1,97 @@
+
+
+
onDragStart?.(row.position)}
+ ondragover={(e) => { e.preventDefault(); onDragOver?.(e, row.position); }}
+ ondrop={(e) => { e.preventDefault(); onDrop?.(e, row.position); }}
+>
+ {#if isOwner}
+
+
+
+ {/if}
+
+
+
+
{format(row.duration_sec)}
+
+ {#if !isUnavailable && liveTrack}
+
+
+ {/if}
+
+ {#if isOwner}
+
+ {/if}
+
diff --git a/web/src/lib/components/PlaylistTrackRow.test.ts b/web/src/lib/components/PlaylistTrackRow.test.ts
new file mode 100644
index 00000000..59ff43b7
--- /dev/null
+++ b/web/src/lib/components/PlaylistTrackRow.test.ts
@@ -0,0 +1,124 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import { readable } from 'svelte/store';
+import type { PlaylistTrack } from '$lib/api/types';
+
+// LikeButton + TrackMenu transitively pull in TanStack Query, the auth
+// store, likes/quarantine APIs, the player store, and SvelteKit
+// navigation. Mock all of them at module level so the component can be
+// rendered in isolation.
+
+vi.mock('$lib/auth/store.svelte', () => ({
+ user: { value: { id: 'u1', username: 'me', is_admin: false } }
+}));
+
+vi.mock('$app/navigation', () => ({
+ goto: vi.fn()
+}));
+
+vi.mock('$lib/api/likes', () => ({
+ createLikedIdsQuery: () =>
+ readable({
+ data: { track_ids: [], album_ids: [], artist_ids: [] },
+ isPending: false,
+ isError: false
+ }),
+ likeEntity: vi.fn().mockResolvedValue(undefined),
+ unlikeEntity: vi.fn().mockResolvedValue(undefined)
+}));
+
+vi.mock('$lib/api/quarantine', () => ({
+ flagTrack: vi.fn().mockResolvedValue({}),
+ unflagTrack: vi.fn().mockResolvedValue(undefined),
+ listMyQuarantine: vi.fn().mockResolvedValue([]),
+ createMyQuarantineQuery: () =>
+ readable({ data: [], isPending: false, isError: false })
+}));
+
+vi.mock('$lib/api/admin/tracks', () => ({
+ removeTrack: vi.fn().mockResolvedValue({ deleted_track_id: 't-1' })
+}));
+
+vi.mock('$lib/player/store.svelte', () => ({
+ playNext: vi.fn(),
+ enqueueTrack: vi.fn(),
+ playQueue: vi.fn(),
+ playRadio: vi.fn()
+}));
+
+vi.mock('@tanstack/svelte-query', async (orig) => {
+ const actual = (await orig()) as Record