Fix warnings and update tracability
This commit is contained in:
@@ -17,6 +17,7 @@ vi.mock("@tauri-apps/api/core", () => ({
|
||||
return {
|
||||
enabled: true,
|
||||
countdownSeconds: 10,
|
||||
maxEpisodes: 5,
|
||||
};
|
||||
}
|
||||
if (command === "player_set_autoplay_settings") {
|
||||
@@ -61,6 +62,7 @@ describe("autoplay API", () => {
|
||||
const settings: AutoplaySettings = {
|
||||
enabled: true,
|
||||
countdownSeconds: 15,
|
||||
maxEpisodes: 5,
|
||||
};
|
||||
|
||||
const result = await setAutoplaySettings(settings);
|
||||
@@ -72,6 +74,7 @@ describe("autoplay API", () => {
|
||||
const settings: AutoplaySettings = {
|
||||
enabled: false,
|
||||
countdownSeconds: 10,
|
||||
maxEpisodes: 5,
|
||||
};
|
||||
|
||||
const result = await setAutoplaySettings(settings);
|
||||
@@ -86,6 +89,7 @@ describe("autoplay API", () => {
|
||||
const settings: AutoplaySettings = {
|
||||
enabled: true,
|
||||
countdownSeconds: 20,
|
||||
maxEpisodes: 5,
|
||||
};
|
||||
|
||||
await setAutoplaySettings(settings);
|
||||
@@ -104,6 +108,7 @@ describe("autoplay API", () => {
|
||||
const settings: AutoplaySettings = {
|
||||
enabled: true,
|
||||
countdownSeconds: countdown,
|
||||
maxEpisodes: 5,
|
||||
};
|
||||
|
||||
const result = await setAutoplaySettings(settings);
|
||||
|
||||
@@ -350,7 +350,7 @@ describe("Backend Integration - Refactored Business Logic", () => {
|
||||
const backendUrl = "https://server.com/Videos/item123/stream.mp4?maxWidth=1280&api_key=token";
|
||||
(invoke as any).mockResolvedValueOnce(backendUrl);
|
||||
|
||||
const url = await client.getVideoDownloadUrl("item123", "720p");
|
||||
const url = await client.getVideoDownloadUrl("item123", "medium");
|
||||
|
||||
expect(url).toBe(backendUrl);
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
@@ -454,8 +454,8 @@ describe("Backend Integration - Refactored Business Logic", () => {
|
||||
it("should support complete flow: load → sort → display", async () => {
|
||||
(invoke as any).mockResolvedValueOnce({
|
||||
items: [
|
||||
{ id: "id1", name: "Album A", sortName: "A" },
|
||||
{ id: "id2", name: "Album B", sortName: "B" },
|
||||
{ id: "id1", name: "Album A" },
|
||||
{ id: "id2", name: "Album B" },
|
||||
],
|
||||
totalRecordCount: 2,
|
||||
});
|
||||
@@ -467,8 +467,8 @@ describe("Backend Integration - Refactored Business Logic", () => {
|
||||
});
|
||||
|
||||
// Backend returned pre-sorted items
|
||||
expect(result.items[0].sortName).toBe("A");
|
||||
expect(result.items[1].sortName).toBe("B");
|
||||
expect(result.items[0].name).toBe("Album A");
|
||||
expect(result.items[1].name).toBe("Album B");
|
||||
|
||||
// Frontend just displays them
|
||||
// No compareFn, no local sorting
|
||||
|
||||
@@ -202,13 +202,13 @@ describe("RepositoryClient", () => {
|
||||
const mockUrl = "https://server.com/Videos/item123/stream.mp4?maxWidth=1920&api_key=token";
|
||||
(invoke as any).mockResolvedValueOnce(mockUrl);
|
||||
|
||||
const downloadUrl = await client.getVideoDownloadUrl("item123", "1080p");
|
||||
const downloadUrl = await client.getVideoDownloadUrl("item123", "high");
|
||||
|
||||
expect(downloadUrl).toBe(mockUrl);
|
||||
expect(invoke).toHaveBeenCalledWith("repository_get_video_download_url", {
|
||||
handle: "test-handle-123",
|
||||
itemId: "item123",
|
||||
quality: "1080p",
|
||||
quality: "high",
|
||||
mediaSourceId: null,
|
||||
});
|
||||
});
|
||||
@@ -228,7 +228,7 @@ describe("RepositoryClient", () => {
|
||||
});
|
||||
|
||||
it("should support quality presets", async () => {
|
||||
const qualities = ["original", "1080p", "720p", "480p"];
|
||||
const qualities = ["original", "high", "medium", "low"];
|
||||
|
||||
for (const quality of qualities) {
|
||||
vi.clearAllMocks();
|
||||
@@ -252,12 +252,12 @@ describe("RepositoryClient", () => {
|
||||
const mockUrl = "https://server.com/Videos/item123/stream.mp4?api_key=token";
|
||||
(invoke as any).mockResolvedValueOnce(mockUrl);
|
||||
|
||||
await client.getVideoDownloadUrl("item123", "720p", "source789");
|
||||
await client.getVideoDownloadUrl("item123", "medium", "source789");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("repository_get_video_download_url", {
|
||||
handle: expect.any(String),
|
||||
itemId: "item123",
|
||||
quality: "720p",
|
||||
quality: "medium",
|
||||
mediaSourceId: "source789",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,9 @@ export interface MediaItem {
|
||||
|
||||
// Cast & Crew
|
||||
people?: Person[];
|
||||
|
||||
// Genres
|
||||
genres?: string[];
|
||||
}
|
||||
|
||||
export type ItemType =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-039 | DR-045 -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
@@ -11,10 +11,16 @@
|
||||
|
||||
let password = $state("");
|
||||
let localError = $state<string | null>(null);
|
||||
let username = $state("User");
|
||||
let serverName = $state("Jellyfin Server");
|
||||
|
||||
const session = auth.getCurrentSession();
|
||||
const username = session?.username ?? "User";
|
||||
const serverName = session?.serverName ?? "Jellyfin Server";
|
||||
// Load session info asynchronously
|
||||
auth.getCurrentSession().then(session => {
|
||||
if (session) {
|
||||
username = session.username ?? "User";
|
||||
serverName = session.serverName ?? "Jellyfin Server";
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
@@ -35,15 +41,14 @@
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
auth.dismissReauth();
|
||||
password = "";
|
||||
localError = null;
|
||||
onDismiss?.();
|
||||
}
|
||||
|
||||
function handleBackdropClick(event: MouseEvent) {
|
||||
function handleBackdropClick(event?: MouseEvent) {
|
||||
// Don't close on backdrop click - require explicit action
|
||||
event.stopPropagation();
|
||||
event?.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-034 | DR-039 -->
|
||||
<script lang="ts">
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import MediaCard from "$lib/components/library/MediaCard.svelte";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-034 | DR-038 -->
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
// Load related artists (by genre)
|
||||
try {
|
||||
if (artist.genres && artist.genres.length > 0) {
|
||||
const relatedResult = await repo.getItems(undefined, {
|
||||
const relatedResult = await repo.getItems(artist.id, {
|
||||
includeItemTypes: ["MusicArtist"],
|
||||
genreIds: artist.genres.slice(0, 2),
|
||||
genres: artist.genres.slice(0, 2),
|
||||
limit: 12,
|
||||
sortBy: "CommunityRating",
|
||||
sortOrder: "Descending"
|
||||
|
||||
@@ -297,7 +297,7 @@ describe.skip("Async Image Loading Pattern", () => {
|
||||
|
||||
const promises = items.map(item =>
|
||||
mockRepository.getImageUrl(item.id, "Primary")
|
||||
.then(url => imageUrls.set(item.id, url))
|
||||
.then((url: string) => imageUrls.set(item.id, url))
|
||||
.catch(() => imageUrls.set(item.id, ""))
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-035 | JA-029 | DR-040, DR-044 -->
|
||||
<script lang="ts">
|
||||
import type { Person, PersonType } from "$lib/api/types";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-035 | JA-029 | DR-040 -->
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { Person } from "$lib/api/types";
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import ResultsCounter from "$lib/components/common/ResultsCounter.svelte";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
|
||||
import type { Genre, MediaItem } from "$lib/api/types";
|
||||
import type { Genre, MediaItem, ItemType } from "$lib/api/types";
|
||||
|
||||
/**
|
||||
* Generic genre browser supporting Movies, Music Albums, and TV Series
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
export interface GenreConfig {
|
||||
itemTypes: string[]; // ["Movie"] or ["MusicAlbum"] or ["Series"]
|
||||
itemTypes: ItemType[]; // ["Movie"] or ["MusicAlbum"] or ["Series"]
|
||||
title: string; // "Movie Genres" or "Genres" or "TV Genres"
|
||||
backPath: string; // "/library" or "/library/music"
|
||||
genreIcon: string; // SVG path for genre icon
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-007, UR-029, UR-030 | DR-007, DR-032, DR-033 -->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
@@ -9,7 +10,7 @@
|
||||
import BackButton from "$lib/components/common/BackButton.svelte";
|
||||
import ResultsCounter from "$lib/components/common/ResultsCounter.svelte";
|
||||
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import type { MediaItem, Library, ItemType } from "$lib/api/types";
|
||||
import LibraryGrid from "./LibraryGrid.svelte";
|
||||
import TrackList from "./TrackList.svelte";
|
||||
|
||||
@@ -23,7 +24,7 @@
|
||||
*/
|
||||
|
||||
export interface MediaListConfig {
|
||||
itemType: string; // "MusicAlbum", "MusicArtist", "Playlist", "Audio"
|
||||
itemType: ItemType; // "MusicAlbum", "MusicArtist", "Playlist", "Audio"
|
||||
title: string; // "Albums", "Artists", "Playlists", "Tracks"
|
||||
backPath: string; // "/library/music"
|
||||
searchPlaceholder?: string;
|
||||
@@ -127,7 +128,7 @@
|
||||
|
||||
const searchPlaceholder = $derived(config.searchPlaceholder || `Search ${config.title.toLowerCase()}...`);
|
||||
|
||||
function handleItemClick(item: MediaItem) {
|
||||
function handleItemClick(item: MediaItem | Library) {
|
||||
// Navigate to detail page for browseable items
|
||||
goto(`/library/${item.id}`);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
describe("Component Initialization", () => {
|
||||
it("should render with title and search bar", () => {
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -67,7 +67,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
|
||||
it("should load items on mount", async () => {
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -87,7 +87,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
|
||||
it("should display sort options", () => {
|
||||
const config = {
|
||||
itemType: "MusicAlbum",
|
||||
itemType: "MusicAlbum" as const,
|
||||
title: "Albums",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search albums...",
|
||||
@@ -115,7 +115,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -169,7 +169,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -215,7 +215,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -260,7 +260,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -309,7 +309,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "MusicAlbum",
|
||||
itemType: "MusicAlbum" as const,
|
||||
title: "Albums",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search albums...",
|
||||
@@ -349,7 +349,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -395,7 +395,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -433,7 +433,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "MusicAlbum",
|
||||
itemType: "MusicAlbum" as const,
|
||||
title: "Albums",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search albums...",
|
||||
@@ -479,7 +479,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -515,7 +515,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -558,7 +558,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
);
|
||||
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -581,7 +581,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
describe("Display Component Props", () => {
|
||||
it("should support grid display component", () => {
|
||||
const config = {
|
||||
itemType: "MusicAlbum",
|
||||
itemType: "MusicAlbum" as const,
|
||||
title: "Albums",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search albums...",
|
||||
@@ -599,7 +599,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
|
||||
it("should support tracklist display component", () => {
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -619,7 +619,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
describe("Config Simplification", () => {
|
||||
it("should not require searchFields in config", () => {
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
@@ -639,7 +639,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
|
||||
it("should not require compareFn in sort options", () => {
|
||||
const config = {
|
||||
itemType: "Audio",
|
||||
itemType: "Audio" as const,
|
||||
title: "Tracks",
|
||||
backPath: "/library/music",
|
||||
searchPlaceholder: "Search tracks...",
|
||||
|
||||
@@ -115,9 +115,11 @@
|
||||
<!-- Download indicator -->
|
||||
{#if showDownloadStatus && (isDownloaded || isDownloading)}
|
||||
{#if isDownloaded}
|
||||
<svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" title="Downloaded">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
|
||||
</svg>
|
||||
<span title="Downloaded">
|
||||
<svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-label="Downloaded">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
|
||||
</svg>
|
||||
</span>
|
||||
{:else if isDownloading}
|
||||
<div class="w-4 h-4 relative flex-shrink-0" title="Downloading...">
|
||||
<svg class="w-4 h-4 -rotate-90" viewBox="0 0 24 24">
|
||||
|
||||
@@ -35,7 +35,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -67,7 +68,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -100,7 +102,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem1 = {
|
||||
id: "item1",
|
||||
name: "Album 1",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "tag1",
|
||||
};
|
||||
|
||||
@@ -118,7 +121,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem2 = {
|
||||
id: "item2",
|
||||
name: "Album 2",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "tag2",
|
||||
};
|
||||
|
||||
@@ -136,7 +140,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -159,7 +164,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
// primaryImageTag is undefined
|
||||
};
|
||||
|
||||
@@ -182,7 +188,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -206,7 +213,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -231,7 +239,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "tag123",
|
||||
};
|
||||
|
||||
@@ -259,7 +268,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
@@ -292,14 +302,16 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const item1 = {
|
||||
id: "item1",
|
||||
name: "Album 1",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "tag1",
|
||||
};
|
||||
|
||||
const item2 = {
|
||||
id: "item2",
|
||||
name: "Album 2",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "tag2",
|
||||
};
|
||||
|
||||
@@ -331,7 +343,8 @@ describe.skip("MediaCard - Async Image Loading", () => {
|
||||
const mediaItem = {
|
||||
id: "item123",
|
||||
name: "Test Album",
|
||||
type: "MusicAlbum",
|
||||
type: "MusicAlbum" as const,
|
||||
serverId: "server-1",
|
||||
primaryImageTag: "abc123",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!-- TRACES: UR-036 | JA-030, JA-031 | DR-041 -->
|
||||
<script lang="ts">
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import type { MediaItem, Library } from "$lib/api/types";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { onMount } from "svelte";
|
||||
import LibraryGrid from "./LibraryGrid.svelte";
|
||||
@@ -39,7 +40,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: MediaItem) {
|
||||
function handleItemClick(item: MediaItem | Library) {
|
||||
goto(`/library/${item.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -322,16 +322,10 @@ describe("TrackList Logic Tests", () => {
|
||||
it("should invoke player when no custom callback", async () => {
|
||||
const invokeMock = (invoke as any).mockResolvedValue(undefined);
|
||||
|
||||
// Simulate unified handler without custom callback
|
||||
const onTrackClick = undefined;
|
||||
if (onTrackClick) {
|
||||
await onTrackClick(mockTracks[0], 0);
|
||||
} else {
|
||||
// This branch executes - default handler
|
||||
await invoke("player_play_queue", {
|
||||
request: { items: [], startIndex: 0, shuffle: false },
|
||||
});
|
||||
}
|
||||
// Simulate unified handler without custom callback - default handler runs
|
||||
await invoke("player_play_queue", {
|
||||
request: { items: [], startIndex: 0, shuffle: false },
|
||||
});
|
||||
|
||||
expect(invokeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -332,8 +332,6 @@ describe.skip("TrackList", () => {
|
||||
// NOTE: This test is skipped because stream URLs are no longer fetched by frontend.
|
||||
// The code now uses player_play_tracks which sends trackIds to backend.
|
||||
// Backend handles all stream URL generation, so this error path no longer exists.
|
||||
|
||||
alertSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-004, UR-005, UR-028 | DR-009 -->
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-005 | DR-009 -->
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { sleepTimerActive } from "$lib/stores/sleepTimer";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-005, UR-017, UR-028 | DR-009 -->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* MiniPlayer component - Always-visible bottom bar audio player
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-026 | DR-029, DR-050 -->
|
||||
<script lang="ts">
|
||||
import {
|
||||
sleepTimer,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
@@ -312,8 +313,8 @@
|
||||
console.log("[VideoPlayer] Video element configured: muted=", videoElement.muted, "volume=", videoElement.volume);
|
||||
|
||||
// DIAGNOSTIC: Check if video has audio tracks
|
||||
if (videoElement.audioTracks) {
|
||||
console.log("[VideoPlayer] Audio tracks count:", videoElement.audioTracks.length);
|
||||
if ((videoElement as any).audioTracks) {
|
||||
console.log("[VideoPlayer] Audio tracks count:", (videoElement as any).audioTracks.length);
|
||||
|
||||
// Set initial audio track (prefer default track)
|
||||
if (selectedAudioTrackIndex === null && audioTracks().length > 0) {
|
||||
@@ -322,11 +323,11 @@
|
||||
console.log("[VideoPlayer] Selected default audio track:", selectedAudioTrackIndex);
|
||||
}
|
||||
}
|
||||
if (videoElement.mozHasAudio !== undefined) {
|
||||
console.log("[VideoPlayer] mozHasAudio:", videoElement.mozHasAudio);
|
||||
if ((videoElement as any).mozHasAudio !== undefined) {
|
||||
console.log("[VideoPlayer] mozHasAudio:", (videoElement as any).mozHasAudio);
|
||||
}
|
||||
if (videoElement.webkitAudioDecodedByteCount !== undefined) {
|
||||
console.log("[VideoPlayer] webkitAudioDecodedByteCount:", videoElement.webkitAudioDecodedByteCount);
|
||||
if ((videoElement as any).webkitAudioDecodedByteCount !== undefined) {
|
||||
console.log("[VideoPlayer] webkitAudioDecodedByteCount:", (videoElement as any).webkitAudioDecodedByteCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -359,7 +360,7 @@
|
||||
const subtitles = media.mediaStreams.filter(s => s.type === "Subtitle");
|
||||
for (const sub of subtitles) {
|
||||
try {
|
||||
const url = getSubtitleUrl(sub.index);
|
||||
const url = await getSubtitleUrl(sub.index);
|
||||
if (url) {
|
||||
subtitleTracks.push({
|
||||
index: sub.index,
|
||||
@@ -617,7 +618,7 @@
|
||||
videoElement?.removeEventListener("seeked", onSeeked);
|
||||
resolve();
|
||||
};
|
||||
videoElement.addEventListener("seeked", onSeeked);
|
||||
videoElement!.addEventListener("seeked", onSeeked);
|
||||
// Fallback timeout in case seeked event doesn't fire
|
||||
setTimeout(() => {
|
||||
videoElement?.removeEventListener("seeked", onSeeked);
|
||||
@@ -951,7 +952,7 @@
|
||||
target: {
|
||||
value: newTime.toString()
|
||||
}
|
||||
} as Event;
|
||||
} as unknown as Event;
|
||||
|
||||
await handleSeekBarChange(syntheticEvent);
|
||||
}
|
||||
@@ -1120,7 +1121,7 @@
|
||||
videoElement?.removeEventListener("canplay", onCanPlay);
|
||||
resolve();
|
||||
};
|
||||
videoElement.addEventListener("canplay", onCanPlay);
|
||||
videoElement!.addEventListener("canplay", onCanPlay);
|
||||
|
||||
// Timeout fallback
|
||||
setTimeout(() => {
|
||||
@@ -1214,11 +1215,11 @@
|
||||
}
|
||||
|
||||
// Get subtitle URL for a given stream index
|
||||
function getSubtitleUrl(streamIndex: number): string {
|
||||
async function getSubtitleUrl(streamIndex: number): Promise<string> {
|
||||
if (!media || !mediaSourceId) return "";
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
return repo.getSubtitleUrl(media.id, mediaSourceId ?? "", streamIndex, "vtt");
|
||||
return await repo.getSubtitleUrl(media.id, mediaSourceId ?? "", streamIndex, "vtt");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-010 | DR-037 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-010 | JA-022, JA-023, JA-024, JA-026 | DR-037 -->
|
||||
<script lang="ts">
|
||||
import type { Session } from "$lib/api/types";
|
||||
import { sessions } from "$lib/stores";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-010 | DR-037 -->
|
||||
<script lang="ts">
|
||||
import type { Session } from "$lib/api/types";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-010 | JA-021, JA-025 | DR-037 -->
|
||||
<script lang="ts">
|
||||
import { sessions, controllableSessions, selectedSession } from "$lib/stores";
|
||||
import { playbackMode, isTransferring, transferError } from "$lib/stores/playbackMode";
|
||||
@@ -86,7 +87,7 @@
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') handleBackdropClick(); }}
|
||||
onkeydown={(e) => { if (e.key === 'Escape' && onClose) onClose(); }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="session-picker-title"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- TRACES: UR-010 | JA-021 | DR-037 -->
|
||||
<script lang="ts">
|
||||
import { sessions, controllableSessions } from "$lib/stores";
|
||||
import SessionCard from "./SessionCard.svelte";
|
||||
|
||||
@@ -38,6 +38,20 @@ export type SyncOperation =
|
||||
* for queueing and managing sync operations.
|
||||
*/
|
||||
class SyncService {
|
||||
/**
|
||||
* Start the sync service (lifecycle managed by Rust backend)
|
||||
*/
|
||||
start(): void {
|
||||
console.log("[SyncService] Started");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the sync service (lifecycle managed by Rust backend)
|
||||
*/
|
||||
stop(): void {
|
||||
console.log("[SyncService] Stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a mutation for sync to server
|
||||
*
|
||||
|
||||
@@ -145,10 +145,10 @@ function createQueueStore() {
|
||||
|
||||
// Get repository handle from auth store
|
||||
const authState = get(auth);
|
||||
if (!authState.isAuthenticated || !authState.repository) {
|
||||
if (!authState.isAuthenticated) {
|
||||
throw new Error("User not authenticated");
|
||||
}
|
||||
const repositoryHandle = authState.repository.getHandle();
|
||||
const repositoryHandle = auth.getRepository().getHandle();
|
||||
|
||||
// Use new Rust commands that accept IDs only
|
||||
if (trackIds.length === 1) {
|
||||
@@ -170,6 +170,10 @@ function createQueueStore() {
|
||||
}
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
set(initialState);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
next,
|
||||
@@ -182,6 +186,7 @@ function createQueueStore() {
|
||||
moveInQueue,
|
||||
syncFromRust,
|
||||
cleanup,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -146,87 +146,13 @@ describe("sessions store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("polling", () => {
|
||||
it.skip("should set isPolling to true when polling starts", async () => {
|
||||
describe("refresh", () => {
|
||||
it("should expose a refresh method for manual session fetching", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
// Mock the refresh to prevent actual API calls
|
||||
vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
// Note: startPolling is not yet implemented
|
||||
// sessions.startPolling(5000);
|
||||
// const state = get(sessions);
|
||||
|
||||
// expect(state.isPolling).toBe(true);
|
||||
});
|
||||
|
||||
it.skip("should set isPolling to false when polling stops", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
// Note: startPolling/stopPolling are not yet implemented
|
||||
// sessions.startPolling(5000);
|
||||
// sessions.stopPolling();
|
||||
// const state = get(sessions);
|
||||
|
||||
// expect(state.isPolling).toBe(false);
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should call refresh immediately when polling starts", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should call refresh at intervals", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
|
||||
// Initial call
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTime(5000);
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Advance another 5 seconds
|
||||
await vi.advanceTimersByTime(5000);
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
sessions.stopPolling();
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should stop previous polling when starting new polling", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
await vi.advanceTimersByTime(5000);
|
||||
|
||||
const callsAfterFirst = refreshSpy.mock.calls.length;
|
||||
|
||||
// Start new polling - should stop the old one
|
||||
sessions.startPolling(3000);
|
||||
|
||||
// Advance by the old interval
|
||||
await vi.advanceTimersByTime(5000);
|
||||
|
||||
// Should have been called once for the new startPolling, and once after 3s
|
||||
expect(refreshSpy.mock.calls.length).toBeGreaterThan(callsAfterFirst);
|
||||
|
||||
sessions.stopPolling();
|
||||
// The store uses event-driven updates via Tauri events,
|
||||
// but also exposes refresh() for manual/on-demand fetching
|
||||
expect(typeof sessions.refresh).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*
|
||||
* @see https://v2.tauri.app/develop/calling-rust/
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe("Tauri Command Parameter Names - Critical Pattern Test", () => {
|
||||
describe("All command parameters use camelCase (Tauri v2 auto-converts)", () => {
|
||||
|
||||
@@ -30,12 +30,12 @@ describe("Real production code - Tauri invoke calls", () => {
|
||||
});
|
||||
|
||||
describe("playbackMode store - real calls", () => {
|
||||
it("playPlayTracks should send repositoryHandle, NOT repository_handle", async () => {
|
||||
it("playbackMode store should be importable and defined", async () => {
|
||||
const { playbackMode } = await import("../stores/playbackMode");
|
||||
|
||||
try {
|
||||
expect(playbackMode).toBeDefined();
|
||||
expect(playbackMode.playPlayTracks).toBeDefined();
|
||||
expect(playbackMode.subscribe).toBeDefined();
|
||||
} catch (e) {
|
||||
// Store might have dependencies we can't mock, but at least we tried
|
||||
}
|
||||
@@ -48,7 +48,7 @@ describe("Real production code - Tauri invoke calls", () => {
|
||||
|
||||
try {
|
||||
expect(sessions).toBeDefined();
|
||||
expect(sessions.sendCommand).toBeDefined();
|
||||
expect(sessions.sendPlayPause).toBeDefined();
|
||||
} catch (e) {
|
||||
// Store might have dependencies we can't mock
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user