import type { ChildProcessWithoutNullStreams } from 'node:child_process'; import { type Mock, type MockedFunction, vi } from 'vitest'; import type { RunYtDlpOptions, runYtDlp } from '../src/services/ytDlpRunner.js'; /** Un appel simulé à yt-dlp, pilotable depuis le test. */ export interface FakeYtDlpRun { /** Arguments reçus par `runYtDlp`. */ readonly args: readonly string[]; /** Options reçues par `runYtDlp`. */ readonly options: RunYtDlpOptions; /** Espions sur les méthodes de stdout du faux processus. */ readonly stdout: { pause: Mock; resume: Mock }; /** `true` dès que le signal d'annulation fourni à `runYtDlp` a été déclenché. */ readonly aborted: boolean; /** Émet une ligne de stdout, comme le ferait le processus. */ emit: (line: string) => void; /** Émet `count` entrées valides supplémentaires (numérotées à la suite des précédentes). */ emitEntries: (count: number) => void; /** Termine le processus avec succès. */ finish: () => void; /** Termine le processus en échec. */ fail: (error: Error) => void; } /** * @param index - Numéro de l'entrée (à partir de 1). * @returns Une ligne `yt-dlp --flat-playlist -j` valide pour cette entrée. */ export function entryLine(index: number): string { return JSON.stringify({ id: `id${index}`, title: `Vidéo ${index}`, url: `https://www.youtube.com/watch?v=id${index}`, duration: 60, thumbnails: [{ url: `https://i.ytimg.com/vi/id${index}/hq.jpg` }], }); } /** * Remplace `runYtDlp` par un faux qui reste « en cours » tant que le test ne * le termine pas, et dont le test pilote la sortie ligne par ligne. * * @param mockedRunYtDlp - Le `runYtDlp` mocké du module testé. * @returns La liste (alimentée au fil des appels) des exécutions simulées. */ export function installFakeYtDlp(mockedRunYtDlp: MockedFunction): FakeYtDlpRun[] { const runs: FakeYtDlpRun[] = []; mockedRunYtDlp.mockImplementation((args, options = {}) => { const stdout = { pause: vi.fn(), resume: vi.fn() }; let settle: { resolve: (value: string) => void; reject: (reason: unknown) => void } | null = null; const promise = new Promise((resolve, reject) => { settle = { resolve, reject }; }); let emitted = 0; const run: FakeYtDlpRun = { args, options, stdout, get aborted() { return options.signal?.aborted ?? false; }, emit: (line) => { options.onStdoutLine?.(line); }, emitEntries: (count) => { for (let i = 0; i < count; i += 1) { emitted += 1; options.onStdoutLine?.(entryLine(emitted)); } }, finish: () => { settle?.resolve(''); }, fail: (error) => { settle?.reject(error); }, }; options.signal?.addEventListener('abort', () => { const error = new Error('yt-dlp annulé'); error.name = 'AbortError'; settle?.reject(error); }); options.onSpawn?.({ stdout } as unknown as ChildProcessWithoutNullStreams); runs.push(run); return promise; }); return runs; }