Files
ersatztv/web/src/builder/ChannelBuilder.test.tsx
T
timothyandClaude Fable 5 d05e442605
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix(web): stop season-tile flooding in channel builder and collections (#180)
Part of the #180 library picker fixes (SPA side).

Channel builder: TelevisionSeason is removed from the library grid fan-out, so a
multi-season show renders as one tile instead of one tile per season. Show tiles
gain a "Seasons" drill-in affordance (both grid and compact layouts) that opens a
dialog listing that show's seasons (via the new GET /api/library/browse?parentId=
&mediaType=TelevisionSeason), each with title, artwork and an Add button that
drops the specific season into the lineup.

Collections add-items dialog: the default search fan-out now excludes seasons,
and a media-kind filter row (All / Movies / Shows / Seasons / Artists,
default = All-without-seasons) keeps seasons reachable when explicitly selected.

Client: getLibraryBrowseItems gains an optional parentId param. Tests cover the
param mapping, the builder no longer requesting TelevisionSeason, and the
collections dialog default-excluding vs explicitly-including seasons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:18 +02:00

65 lines
2.2 KiB
TypeScript

import { cleanup, render, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ChannelBuilderScreen } from './ChannelBuilder';
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
headers: { 'Content-Type': 'application/json' },
status
});
}
// Mocks every endpoint the builder loads on mount so it can render its library
// browser, plus /api/library/browse which the browse hook fans out across.
function mockBuilderApi() {
return vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL) => {
const url = input.toString();
if (url.startsWith('/api/library/browse')) {
return Promise.resolve(jsonResponse({ page: [], totalCount: 0 }));
}
if (url === '/api/channel-templates/default') {
// Builder treats a 404 here as "no default template".
return Promise.resolve(jsonResponse({ status: 404, title: 'Not Found' }, 404));
}
// channels, channel-templates, ffmpeg/profiles, filler-presets, watermarks, media-sources
return Promise.resolve(jsonResponse([]));
});
}
function browseMediaTypes(fetchMock: ReturnType<typeof mockBuilderApi>): string[] {
return fetchMock.mock.calls
.map(([u]) => u.toString())
.filter((u) => u.startsWith('/api/library/browse'))
.map((u) => new URL(u, 'http://localhost').searchParams.get('mediaType'))
.filter((mediaType): mediaType is string => mediaType != null && mediaType !== '');
}
describe('ChannelBuilder library browse', () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
beforeEach(() => {
window.localStorage.clear();
});
it('fans out over movies/shows/artists and never requests TelevisionSeason', async () => {
const fetchMock = mockBuilderApi();
render(<ChannelBuilderScreen />);
// Wait until the initial library browse fan-out has fired.
await waitFor(() => {
expect(browseMediaTypes(fetchMock).length).toBeGreaterThan(0);
});
const types = new Set(browseMediaTypes(fetchMock));
expect(types).toEqual(new Set(['Movie', 'TelevisionShow', 'Artist']));
expect(types.has('TelevisionSeason')).toBe(false);
});
});