Files
ersatztv/web/src/api/ffmpegProfiles.test.ts
T
timothyandClaude Fable 5 e95eed11dd feat(web): FFmpeg profile, filler preset, watermark editors (#143)
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>
2026-07-07 14:33:01 +02:00

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' })
);
});
});