import { expect, type Page, type Route } from "@playwright/test"; export async function fulfillJson(route: Route, status: number, body: unknown): Promise { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body), }); } export async function fulfillEmpty(route: Route, status = 204): Promise { await route.fulfill({ status, body: "", }); } export interface EngineStatusResponse { status: "running" | "paused" | "draining"; manual_paused: boolean; scheduler_paused: boolean; draining: boolean; mode: "background" | "balanced" | "throughput"; concurrent_limit: number; is_manual_override: boolean; } export interface EngineModeResponse { mode: "background" | "balanced" | "throughput"; is_manual_override: boolean; concurrent_limit: number; cpu_count: number; computed_limits: { background: number; balanced: number; throughput: number; }; } export interface DashboardStatsResponse { active: number; concurrent_limit: number; completed: number; failed: number; total: number; } export interface SystemSettingsResponse { monitoring_poll_interval: number; enable_telemetry: boolean; watch_enabled?: boolean; } export interface SystemResourcesResponse { cpu_percent: number; memory_used_mb: number; memory_total_mb: number; memory_percent: number; uptime_seconds: number; active_jobs: number; concurrent_limit: number; cpu_count: number; gpu_utilization: number | null; gpu_memory_percent: number | null; } export interface NotificationTargetFixture { id: number; name: string; target_type: "discord" | "gotify" | "webhook"; endpoint_url: string; auth_token: string | null; events: string; enabled: boolean; } export interface ScheduleWindowFixture { id: number; start_time: string; end_time: string; days_of_week: string; enabled: boolean; } export interface WatchDirFixture { id: number; path: string; is_recursive: boolean; profile_id: number | null; } export interface LibraryProfileFixture { id: number; name: string; preset: string; codec: "av1" | "hevc" | "h264"; quality_profile: "speed" | "balanced" | "quality"; hdr_mode: "preserve" | "tonemap"; audio_mode: "copy" | "aac" | "aac_stereo"; crf_override: number | null; notes: string | null; builtin: boolean; } export interface JobFixture { id: number; input_path: string; output_path: string; status: string; priority: number; progress: number; created_at: string; updated_at: string; attempt_count?: number; vmaf_score?: number; decision_reason?: string; } export interface JobDetailFixture { job: JobFixture; metadata?: { duration_secs: number; codec_name: string; width: number; height: number; bit_depth?: number; size_bytes: number; video_bitrate_bps?: number; container_bitrate_bps?: number; fps: number; container: string; audio_codec?: string; audio_channels?: number; dynamic_range?: string; }; encode_stats?: { input_size_bytes: number; output_size_bytes: number; compression_ratio: number; encode_time_seconds: number; encode_speed: number; avg_bitrate_kbps: number; vmaf_score?: number; }; job_logs?: Array<{ id: number; level: string; message: string; created_at: string; }>; job_failure_summary?: string; } interface SettingsBundle { settings: { appearance: { active_theme_id: string | null }; scanner: { directories: string[]; watch_enabled: boolean; extra_watch_dirs: string[]; }; transcode: { concurrent_jobs: number; size_reduction_threshold: number; min_bpp_threshold: number; min_file_size_mb: number; output_codec: "av1" | "hevc" | "h264"; quality_profile: "speed" | "balanced" | "quality"; allow_fallback: boolean; subtitle_mode: "copy" | "burn" | "extract" | "none"; }; hardware: { allow_cpu_encoding: boolean; allow_cpu_fallback: boolean; preferred_vendor: string | null; cpu_preset: string; device_path: string | null; }; files: { delete_source: boolean; output_extension: string; output_suffix: string; replace_strategy: string; output_root: string | null; }; quality: { enable_vmaf: boolean; min_vmaf_score: number; revert_on_low_quality: boolean; }; notifications: { enabled: boolean; targets: NotificationTargetFixture[]; }; schedule: { windows: ScheduleWindowFixture[]; }; system: { enable_telemetry: boolean; monitoring_poll_interval: number; }; }; } export function createEngineStatus( overrides: Partial = {}, ): EngineStatusResponse { return { status: "paused", manual_paused: true, scheduler_paused: false, draining: false, mode: "balanced", concurrent_limit: 2, is_manual_override: false, ...overrides, }; } export function createEngineMode( overrides: Partial = {}, ): EngineModeResponse { return { mode: "balanced", is_manual_override: false, concurrent_limit: 2, cpu_count: 8, computed_limits: { background: 1, balanced: 4, throughput: 4, }, ...overrides, }; } export function createSettingsBundle( overrides: Partial = {}, ): SettingsBundle { return { settings: { appearance: { active_theme_id: "helios-orange", ...(overrides.appearance ?? {}), }, scanner: { directories: ["/media/movies"], watch_enabled: true, extra_watch_dirs: [], ...(overrides.scanner ?? {}), }, transcode: { concurrent_jobs: 2, size_reduction_threshold: 0.3, min_bpp_threshold: 0.1, min_file_size_mb: 100, output_codec: "av1", quality_profile: "balanced", allow_fallback: true, subtitle_mode: "copy", ...(overrides.transcode ?? {}), }, hardware: { allow_cpu_encoding: true, allow_cpu_fallback: true, preferred_vendor: null, cpu_preset: "medium", device_path: null, ...(overrides.hardware ?? {}), }, files: { delete_source: false, output_extension: "mkv", output_suffix: "-alchemist", replace_strategy: "keep", output_root: null, ...(overrides.files ?? {}), }, quality: { enable_vmaf: false, min_vmaf_score: 90, revert_on_low_quality: true, ...(overrides.quality ?? {}), }, notifications: { enabled: false, targets: [], ...(overrides.notifications ?? {}), }, schedule: { windows: [], ...(overrides.schedule ?? {}), }, system: { enable_telemetry: false, monitoring_poll_interval: 2, ...(overrides.system ?? {}), }, }, }; } export async function mockSettingsBundle( page: Page, overrides: Partial = {}, ): Promise { const bundle = createSettingsBundle(overrides); await page.route("**/api/settings/bundle", async (route) => { await fulfillJson(route, 200, bundle); }); } export async function mockDashboardData( page: Page, options: { jobs?: JobFixture[]; stats?: Partial; systemSettings?: Partial; resources?: Partial; bundle?: Partial; } = {}, ): Promise { const stats: DashboardStatsResponse = { active: 0, concurrent_limit: 2, completed: 0, failed: 0, total: 0, ...(options.stats ?? {}), }; const systemSettings: SystemSettingsResponse = { monitoring_poll_interval: 2, enable_telemetry: false, watch_enabled: true, ...(options.systemSettings ?? {}), }; const resources: SystemResourcesResponse = { cpu_percent: 8, memory_used_mb: 1024, memory_total_mb: 8192, memory_percent: 12.5, uptime_seconds: 3600, active_jobs: stats.active, concurrent_limit: stats.concurrent_limit, cpu_count: 8, gpu_utilization: 0, gpu_memory_percent: 0, ...(options.resources ?? {}), }; await mockJobsTable(page, options.jobs ?? []); await mockSettingsBundle(page, options.bundle ?? {}); await page.route("**/api/stats", async (route) => { await fulfillJson(route, 200, stats); }); await page.route("**/api/settings/system", async (route) => { await fulfillJson(route, 200, systemSettings); }); await page.route("**/api/system/resources", async (route) => { await fulfillJson(route, 200, resources); }); } export async function mockEngineStatus( page: Page, statusOverrides: Partial = {}, modeOverrides: Partial = {}, ): Promise { const status = createEngineStatus(statusOverrides); const mode = createEngineMode({ mode: status.mode, concurrent_limit: status.concurrent_limit, is_manual_override: status.is_manual_override, ...modeOverrides, }); await page.route("**/api/engine/status", async (route) => { await fulfillJson(route, 200, status); }); await page.route("**/api/engine/mode", async (route) => { await fulfillJson(route, 200, mode); }); } export async function mockJobsTable( page: Page, jobs: JobFixture[], onRequest?: (url: URL) => void, ): Promise { await page.route("**/api/jobs/table**", async (route) => { const url = new URL(route.request().url()); onRequest?.(url); await fulfillJson(route, 200, jobs); }); } export async function mockJobDetails( page: Page, details: Record, ): Promise { await page.route("**/api/jobs/*/details", async (route) => { const match = route.request().url().match(/\/api\/jobs\/(\d+)\/details/); const id = match ? Number.parseInt(match[1], 10) : NaN; const detail = details[id]; if (!detail) { await fulfillJson(route, 404, { message: "not found" }); return; } await fulfillJson(route, 200, detail); }); } export async function mockSetupBootstrap( page: Page, options: { setupRequired?: boolean; configMutable?: boolean; bundle?: Partial; recommendations?: Array<{ path: string; label: string; reason: string; media_hint: string; }>; hardware?: { vendor: string; device_path: string | null; supported_codecs: string[]; }; } = {}, ): Promise { await page.route("**/api/setup/status", async (route) => { await fulfillJson(route, 200, { setup_required: options.setupRequired ?? true, config_mutable: options.configMutable ?? true, enable_telemetry: false, }); }); await page.route("**/api/system/hardware", async (route) => { await fulfillJson( route, 200, options.hardware ?? { vendor: "Cpu", device_path: null, supported_codecs: ["h264", "hevc", "av1"], }, ); }); await mockSettingsBundle(page, options.bundle ?? {}); await page.route("**/api/fs/recommendations", async (route) => { await fulfillJson(route, 200, { recommendations: options.recommendations ?? [], }); }); } export async function expectVisibleError(page: Page, message: string): Promise { await expect(page.getByText(message).first()).toBeVisible(); }