Three new SPA list+editor surfaces, closing the Blazor-only gap found in the #91 phase-(b) recon: - FFmpeg Profiles (System group): list with resolution/video/audio summary + add/copy/delete; full General/Video/Audio editor. Per-hwaccel video-format and profile rules ported from FFmpegProfileEditViewModelValidator; video preset list ported from FFmpegLibraryHelper.PresetsForFFmpegProfile / AvailablePresets.ForAccelAndFormat (cited in code). Resolutions from /api/settings/resolutions, hwaccel kinds from the new lookup endpoint. Copy = client-side clone into the create form (no server copy endpoint). - Filler Presets (Media group): list + editor with the Blazor VM interlocks (Fallback/Tail force mode None; non-MidRoll clears expression; Fallback disables chapters; Playlist collection type forces Count mode; duration/ count/pad fields gated by mode). Collection pickers use the existing /api/library/browse client for all seven collection types incl. Playlist. - Watermarks (System group): list + editor with mode-driven field enabling, ChannelLogo vs Custom image source, upload via /api/artwork/uploads (target=watermark) and preview via /artwork/watermarks/{path}. - App.tsx: ScreenIds/routes (sub-paths /add + /{id}) and nav registration; api modules ffmpegProfiles/fillerPresets/watermarks with URL-assert tests; SettingsScreen Classic-UI help text updated (these are no longer Blazor-only) and the FFmpeg-profiles card now links to the new screen. - Regenerated web/src/api/generated/v1.d.ts from the #159 OpenAPI changes. web verification: typecheck clean; vitest 188/188 (14 files); vite build OK; eslint clean on touched files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
createFFmpegProfile,
|
|
deleteFFmpegProfile,
|
|
getFFmpegProfile,
|
|
getHardwareAccelerationKinds,
|
|
updateFFmpegProfile,
|
|
type CreateFFmpegProfileRequest
|
|
} from './ffmpegProfiles';
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
status
|
|
});
|
|
}
|
|
|
|
function noContent(): Response {
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
const sampleRequest: CreateFFmpegProfileRequest = {
|
|
allowBFrames: false,
|
|
audioBitrate: 192,
|
|
audioBufferSize: 384,
|
|
audioChannels: 2,
|
|
audioFormat: 'Aac',
|
|
audioSampleRate: 48,
|
|
bitDepth: 'EightBit',
|
|
deinterlaceVideo: false,
|
|
hardwareAcceleration: 'None',
|
|
name: 'New',
|
|
normalizeAudio: true,
|
|
normalizeColors: true,
|
|
normalizeFramerate: false,
|
|
normalizeLoudnessMode: 'Off',
|
|
normalizeVideo: true,
|
|
padMode: 'Software',
|
|
qsvExtraHardwareFrames: null,
|
|
resolutionId: 1,
|
|
scalingBehavior: 'ScaleAndPad',
|
|
tonemapAlgorithm: 'Linear',
|
|
targetLoudness: null,
|
|
threadCount: 0,
|
|
vaapiDevice: '',
|
|
vaapiDisplay: '',
|
|
vaapiDriver: 'Default',
|
|
videoBitrate: 2000,
|
|
videoBufferSize: 4000,
|
|
videoFormat: 'H264',
|
|
videoPreset: '',
|
|
videoProfile: 'high'
|
|
};
|
|
|
|
describe('ffmpeg profiles api client', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('getFFmpegProfile fetches a profile by id', async () => {
|
|
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, name: 'Default' }));
|
|
|
|
await expect(getFFmpegProfile(4)).resolves.toMatchObject({ id: 4 });
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/ffmpeg/profiles/4', expect.objectContaining({ method: 'GET' }));
|
|
});
|
|
|
|
it('createFFmpegProfile POSTs the request body', async () => {
|
|
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 7, name: 'New' }, 201));
|
|
|
|
await expect(createFFmpegProfile(sampleRequest)).resolves.toMatchObject({ id: 7 });
|
|
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
expect(url).toBe('/api/ffmpeg/profiles');
|
|
expect(init).toMatchObject({ method: 'POST' });
|
|
expect(JSON.parse(String(init?.body)).resolutionId).toBe(1);
|
|
});
|
|
|
|
it('updateFFmpegProfile PUTs to the id route', async () => {
|
|
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 3, name: 'Edited' }));
|
|
|
|
await updateFFmpegProfile(3, sampleRequest);
|
|
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
expect(url).toBe('/api/ffmpeg/profiles/3');
|
|
expect(init).toMatchObject({ method: 'PUT' });
|
|
});
|
|
|
|
it('deleteFFmpegProfile issues a DELETE and resolves on 204', async () => {
|
|
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
|
|
|
await expect(deleteFFmpegProfile(9)).resolves.toBeUndefined();
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/ffmpeg/profiles/9', expect.objectContaining({ method: 'DELETE' }));
|
|
});
|
|
|
|
it('getHardwareAccelerationKinds fetches the lookup', async () => {
|
|
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(['None', 'Nvenc']));
|
|
|
|
await expect(getHardwareAccelerationKinds()).resolves.toEqual(['None', 'Nvenc']);
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'/api/ffmpeg/hardware-acceleration-kinds',
|
|
expect.objectContaining({ method: 'GET' })
|
|
);
|
|
});
|
|
});
|