// Tests hors réseau de backend/src/lyrics/Lyrics.js (node lyrics.test.js) : fetch global remplacé par un faux const { check, done, req } = require("./_setup"); const L = req("src/lyrics/Lyrics.js"); const realFetch = globalThis.fetch; let calls = []; let route = () => json(404, { message: "not found" }); function json(status, body) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } function mock(handler) { calls = []; route = handler; L.clearCache(); } globalThis.fetch = async (input, options = {}) => { const url = new URL(String(input)); calls.push({ url, options }); return route(url, options); }; const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b); const LRC = "[ar:Daft Punk]\n[ti:One More Time]\n[00:30.75] One more time\n[00:33.18] \n[00:46.35] Celebrate\n[00:50.00] Oh yeah\n"; const PLAIN = "One more time\n\nCelebrate\nOh yeah"; function record(over = {}) { return { id: 1, trackName: "One More Time", artistName: "Daft Punk", albumName: "Discovery", duration: 320, instrumental: false, plainLyrics: PLAIN, syncedLyrics: LRC, ...over, }; } (async () => { console.log("parseLrc"); { check("texte non chaîne → []", eq(L.parseLrc(null), []) && eq(L.parseLrc(42), []) && eq(L.parseLrc(""), [])); const lines = L.parseLrc(LRC); check("métadonnées ignorées, lignes vides gardées", eq(lines, [ { t: 30750, text: "One more time" }, { t: 33180, text: "" }, { t: 46350, text: "Celebrate" }, { t: 50000, text: "Oh yeah" }, ]), lines); check("[mm:ss] sans fraction", eq(L.parseLrc("[01:02]Bye"), [{ t: 62000, text: "Bye" }])); check("fractions .x / .xx / .xxx / :xx", eq(L.parseLrc("[00:01.5]a\n[00:02.25]b\n[00:03.125]c\n[00:04:50]d").map(l => l.t), [1500, 2250, 3125, 4500])); const multi = L.parseLrc("[00:20.00][00:05.00]Refrain\n[00:10.00]Couplet"); check("plusieurs horodatages par ligne + tri", eq(multi, [ { t: 5000, text: "Refrain" }, { t: 10000, text: "Couplet" }, { t: 20000, text: "Refrain" }, ]), multi); check("[offset:+500] avance les paroles", eq(L.parseLrc("[offset:+500]\n[00:10.00]a").map(l => l.t), [9500])); check("[offset:-250] retarde les paroles", eq(L.parseLrc("[offset:-250]\n[00:10.00]a").map(l => l.t), [10250])); check("offset ne rend pas de temps négatif", eq(L.parseLrc("[offset:2000]\n[00:01.00]a").map(l => l.t), [0])); check("CRLF, BOM et horodatages par mot", eq(L.parseLrc("[00:01.00]Hello <00:01.20>world\r\n[00:02.00] x "), [ { t: 1000, text: "Hello world" }, { t: 2000, text: "x" }, ])); check("lignes sans horodatage ignorées, secondes invalides ignorées", eq(L.parseLrc("texte libre\n[00:75.00]x\n[00:03.00]ok"), [{ t: 3000, text: "ok" }])); check("égalité de temps : ordre d'origine conservé", eq(L.parseLrc("[00:01.00]a\n[00:01.00]b").map(l => l.text), ["a", "b"])); } console.log("cleanTitle / cleanArtist"); { check("Official Video + [HD]", L.cleanTitle("One More Time (Official Video) [HD]") === "One More Time"); check("Clip officiel / Lyrics", L.cleanTitle("Alors on danse (Clip Officiel)") === "Alors on danse" && L.cleanTitle("Hello (Lyrics)") === "Hello"); check("feat. retiré", L.cleanTitle("Get Lucky (feat. Pharrell Williams)") === "Get Lucky" && L.cleanTitle("Get Lucky ft. Pharrell") === "Get Lucky"); check("suffixe « - Remastered 2011 » retiré", L.cleanTitle("Bohemian Rhapsody - Remastered 2011") === "Bohemian Rhapsody"); check("suffixe « | … » retiré", L.cleanTitle("Titre | Clip officiel") === "Titre"); check("parenthèse de titre conservée", L.cleanTitle("(Don't Fear) The Reaper") === "(Don't Fear) The Reaper"); check("« (Remix) » conservé", L.cleanTitle("Song (Remix)") === "Song (Remix)"); check("« Video » dans un vrai titre conservé", L.cleanTitle("The Buggles - Video Killed the Radio Star") === "The Buggles - Video Killed the Radio Star"); check("titre entièrement parasite → original", L.cleanTitle("(Official Video)") === "(Official Video)"); check("vide / null", L.cleanTitle("") === "" && L.cleanTitle(null) === ""); check("Artist - Topic", L.cleanArtist("Stromae - Topic") === "Stromae"); check("ArtistVEVO → espaces", L.cleanArtist("DaftPunkVEVO") === "Daft Punk" && L.cleanArtist("Adele VEVO") === "Adele"); check("Official / feat.", L.cleanArtist("Daft Punk Official") === "Daft Punk" && L.cleanArtist("Daft Punk feat. Pharrell") === "Daft Punk"); check("Auteur inconnu → vide", L.cleanArtist("Auteur inconnu") === ""); check("AC/DC intact", L.cleanArtist("AC/DC") === "AC/DC"); check("longueur bornée à 200", L.cleanTitle("a".repeat(500)).length === 200); } console.log("guessQuery"); { const g = (title, author, extra = {}) => L.guessQuery({ title, author, type: "youtube", ...extra }); check("Artiste - Titre (Official Video)", eq(g("Daft Punk - One More Time (Official Video)", "Daft Punk", { duration: 320.4 }), { track: "One More Time", artist: "Daft Punk", duration: 320 })); check("chaîne VEVO sans tiret", eq(g("Never Gonna Give You Up", "RickAstleyVEVO"), { track: "Never Gonna Give You Up", artist: "Rick Astley", duration: 0 })); check("chaîne de label → artiste du titre", eq(g("The Buggles - Video Killed the Radio Star", "Island Records"), { track: "Video Killed the Radio Star", artist: "The Buggles", duration: 0 })); check("Titre - Artiste", eq(g("Shape of You - Ed Sheeran", "Ed Sheeran"), { track: "Shape of You", artist: "Ed Sheeran", duration: 0 })); check("suffixe de version Spotify", eq(g("Hello - Live at Wembley", "Adele"), { track: "Hello", artist: "Adele", duration: 0 })); check("feat. dans la partie artiste", eq(g("Daft Punk ft. Pharrell Williams - Get Lucky [HD] | Clip officiel", "Some Label"), { track: "Get Lucky", artist: "Daft Punk", duration: 0 })); check("Artiste « Titre »", eq(g('Stromae "Alors on danse" (Clip officiel)', "StromaeVEVO"), { track: "Alors on danse", artist: "Stromae", duration: 0 })); check("guillemets dans un titre Spotify", eq(g('Theme from "Shaft"', "Isaac Hayes"), { track: "Theme from Shaft", artist: "Isaac Hayes", duration: 0 })); check("fichier envoyé", eq(L.guessQuery({ title: "01 - Muse - Uprising.mp3", author: "Auteur inconnu", type: "attachment" }), { track: "Uprising", artist: "Muse", duration: 0 })); check("valeurs par défaut du Song → vide", eq(L.guessQuery({ title: "Aucun titre", author: "Auteur inconnu" }), { track: "", artist: "", duration: 0 })); check("durée invalide → 0", L.guessQuery({ title: "x", duration: "abc" }).duration === 0 && L.guessQuery({ title: "x", duration: -3 }).duration === 0); check("pas d'objet → vide", eq(L.guessQuery(null), { track: "", artist: "", duration: 0 })); } console.log("songKeyOf"); { check("type:id", L.songKeyOf({ type: "youtube", id: "abc", url: "u" }) === "youtube:abc"); check("repli sur l'URL", L.songKeyOf({ type: "youtube", url: "https://x" }) === "https://x"); check("rien → null", L.songKeyOf({}) === null && L.songKeyOf(null) === null); } console.log("pickBestCandidate"); { const q = { track: "One More Time", artist: "Daft Punk", duration: 320 }; const list = [ record({ id: 10, duration: 234, albumName: "Charts" }), record({ id: 11, duration: 321, syncedLyrics: null }), record({ id: 12, duration: 319 }), record({ id: 13, trackName: "Around the World", duration: 320 }), record({ id: 14, artistName: "Someone Else", duration: 330 }), ]; check("synchronisé et durée proche", L.pickBestCandidate(list, q)?.id === 12); check("titre différent rejeté", L.pickBestCandidate([list[3]], q) === null); check("autre artiste rejeté", L.pickBestCandidate([list[4]], q) === null); // Chaîne de label : titre identique et durée quasi exacte suffisent check("autre artiste accepté si titre et durée exacts", L.pickBestCandidate([record({ id: 19, artistName: "Ed Banger Records", duration: 321 })], q)?.id === 19); check("hors fenêtre seulement si rien de mieux", L.pickBestCandidate([list[0]], q)?.id === 10 && L.pickBestCandidate([list[0], list[1]], q)?.id === 11); check("sans contenu ignoré", L.pickBestCandidate([record({ id: 15, syncedLyrics: "", plainLyrics: "" })], q) === null); const upload = record({ id: 16, trackName: "Daft Punk - One More Time (Official Video)" }); check("saisie propre préférée à « Artiste - Titre (Official Video) »", L.pickBestCandidate([upload, record({ id: 17 })], q)?.id === 17); const free = { track: "daft punk one more time", artist: "", duration: 0 }; check("texte libre : bon candidat", L.pickBestCandidate([list[3], record({ id: 18 })], free)?.id === 18); check("texte libre : titre absent de la requête rejeté", L.pickBestCandidate([list[3]], free) === null); } console.log("getLyricsForSong (fetch simulé)"); { const song = { type: "youtube", id: "FGBhQbmPwH8", title: "Daft Punk - One More Time (Official Video)", author: "Daft Punk", duration: 320, url: "https://www.youtube.com/watch?v=FGBhQbmPwH8" }; mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {})); let r = await L.getLyricsForSong(song); check("LRCLIB /get → synchronisées", r.ok && r.source === "lrclib" && r.synced && r.lines.length === 4 && r.lines[0].t === 30750, r); check("songKey renvoyé", r.songKey === "youtube:FGBhQbmPwH8"); check("plain et match remplis", r.plain === PLAIN && eq(r.match, { id: 1, track: "One More Time", artist: "Daft Punk", album: "Discovery", duration: 320 }) && r.instrumental === false); const p = calls[0].url.searchParams; check("paramètres /get nettoyés", calls[0].url.host === "lrclib.net" && p.get("artist_name") === "Daft Punk" && p.get("track_name") === "One More Time" && p.get("duration") === "320", calls[0].url.href); check("User-Agent avec la version", /^Subsonics-Chopin\/\S+ \(https:\/\/git\.raphix\.fr\/subsonics\)$/.test(calls[0].options.headers["User-Agent"]) && L.USER_AGENT.includes(require("../../package.json").version)); check("délai d'attente fourni", calls[0].options.signal instanceof AbortSignal); const before = calls.length; const again = await L.getLyricsForSong({ ...song, id: "autreVideo" }); check("cache par requête (aucun nouvel appel), songKey propre", calls.length === before && again.songKey === "youtube:autreVideo" && again.synced); mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {})); r = await L.getLyricsForSong({ ...song, duration: 0 }); check("durée 0 → pas de paramètre duration", r.ok && !calls[0].url.searchParams.has("duration")); mock((url) => { if (url.pathname === "/api/get") return json(404, { message: "Failed to find specified track" }); if (url.pathname === "/api/search" && url.searchParams.get("track_name")) { return json(200, [record({ id: 30, duration: 210 }), record({ id: 31, duration: 321 }), record({ id: 32, trackName: "Digital Love", duration: 320 })]); } return json(200, []); }); r = await L.getLyricsForSong(song); check("/get 404 → recherche par champs, bon candidat", r.ok && r.match.id === 31 && r.synced, r.match); check("recherche libre évitée si un candidat convient", !calls.some(c => c.url.searchParams.has("q"))); mock((url) => { if (url.pathname === "/api/get") return json(404, {}); if (url.searchParams.has("q")) return json(200, [record({ id: 40, duration: 320 })]); return json(200, []); }); r = await L.getLyricsForSong(song); check("puis recherche libre « artiste titre »", r.ok && r.match.id === 40 && calls.find(c => c.url.searchParams.has("q"))?.url.searchParams.get("q") === "Daft Punk One More Time"); mock((url) => url.pathname === "/api/get" ? json(200, record({ duration: 260 })) : json(200, [])); r = await L.getLyricsForSong(song); check("durée trop différente → texte seul", r.ok && r.synced === false && r.lines === null && r.plain === PLAIN, r); mock((url) => url.pathname === "/api/get" ? json(200, record({ duration: 260, plainLyrics: null })) : json(200, [])); r = await L.getLyricsForSong(song); check("texte seul reconstruit depuis le LRC", r.ok && !r.synced && r.plain === PLAIN, r.plain); mock((url) => url.pathname === "/api/get" ? json(200, record({ instrumental: true, plainLyrics: null, syncedLyrics: null })) : json(404, {})); r = await L.getLyricsForSong(song); check("instrumental", r.ok && r.instrumental && !r.synced && r.lines === null && r.plain === null); // Entrée d'essai réellement présente sur LRCLIB (« [00:00.00]probe ») : ignorée au profit de la recherche mock((url) => { if (url.pathname === "/api/get") return json(200, record({ id: 50, plainLyrics: "probe", syncedLyrics: "[00:00.00]probe" })); if (url.pathname === "/api/search") return json(200, [record({ id: 50, plainLyrics: "probe", syncedLyrics: "[00:00.00]probe" }), record({ id: 51 })]); return json(404, {}); }); r = await L.getLyricsForSong(song); check("entrée vandalisée ignorée", r.ok && r.match.id === 51 && r.synced, r.match); // 503 passager : une seule nouvelle tentative let flaky = 0; mock((url) => { if (url.pathname === "/api/get") return ++flaky === 1 ? json(503, {}) : json(200, record()); return json(404, {}); }); r = await L.getLyricsForSong(song); check("503 passager → nouvelle tentative réussie", r.ok && r.synced && flaky === 2); mock((url) => { if (url.host === "lrclib.net") return url.pathname === "/api/get" ? json(404, {}) : json(200, []); if (url.host === "api.deezer.com") return json(200, { data: [ { id: 1, title: "Harder, Better, Faster, Stronger", artist: { name: "Daft Punk" }, album: { title: "Discovery" }, duration: 224 }, { id: 2, title: "One More Time (Radio Edit)", title_short: "One More Time", artist: { name: "Daft Punk" }, album: { title: "Discovery" }, duration: 320 }, ] }); if (url.host === "api.lyrics.ovh") return json(200, { lyrics: "Paroles de la chanson One More Time par Daft Punk\r\nOne more time\r\n\r\n\r\nCelebrate\r\nOh yeah" }); return json(404, {}); }); r = await L.getLyricsForSong(song); check("repli Deezer + lyrics.ovh", r.ok && r.source === "lyrics.ovh" && !r.synced && r.lines === null && r.plain === PLAIN, r); const ovh = calls.find(c => c.url.host === "api.lyrics.ovh"); check("lyrics.ovh interrogé avec le bon candidat Deezer", ovh && decodeURIComponent(ovh.url.pathname) === "/v1/Daft Punk/One More Time", ovh?.url.pathname); check("Deezer en https", calls.find(c => c.url.host === "api.deezer.com")?.url.protocol === "https:"); mock(() => json(404, { error: "No lyrics found" })); r = await L.getLyricsForSong(song); check("rien nulle part → NOT_FOUND + songKey", !r.ok && r.error === "NOT_FOUND" && r.songKey === "youtube:FGBhQbmPwH8", r); const n = calls.length; await L.getLyricsForSong(song); check("échec mis en cache", calls.length === n); mock((url) => { if (url.host === "lrclib.net") throw new TypeError("fetch failed"); return json(404, {}); }); r = await L.getLyricsForSong(song); check("LRCLIB injoignable → repli puis UNAVAILABLE", !r.ok && r.error === "UNAVAILABLE" && calls.filter(c => c.url.host === "lrclib.net").length === 2 && calls.every(c => c.url.pathname !== "/api/search"), calls.map(c => c.url.href)); mock((url) => url.host === "lrclib.net" ? json(500, {}) : json(404, {})); r = await L.getLyricsForSong(song); check("HTTP 500 → UNAVAILABLE", !r.ok && r.error === "UNAVAILABLE"); mock(() => json(200, record())); check("pas de morceau → NO_MUSIC", eq(await L.getLyricsForSong(null), { ok: false, error: "NO_MUSIC" })); r = await L.getLyricsForSong({ type: "youtube", id: "x", title: "Aucun titre", author: "Auteur inconnu" }); check("titre vide → INVALID_PARAMS", !r.ok && r.error === "INVALID_PARAMS" && r.songKey === "youtube:x" && calls.length === 0); } console.log("searchCandidates / getById / findLyrics / getLyrics"); { mock(() => json(200, record())); for (const bad of [undefined, null, 42, "", " ", "x".repeat(201), { q: "x" }]) { const r = await L.searchCandidates(bad); check(`searchCandidates(${JSON.stringify(bad)?.slice(0, 20)}) → INVALID_PARAMS`, eq(r, { ok: false, error: "INVALID_PARAMS" })); } check("aucune requête pour une entrée invalide", calls.length === 0); mock(() => json(200, [record({ id: 5 }), record({ id: 6, syncedLyrics: null, instrumental: true, albumName: null }), { bad: true }])); let r = await L.searchCandidates(" daft punk one more time "); check("candidats mis en forme", r.ok && eq(r.results, [ { id: 5, track: "One More Time", artist: "Daft Punk", album: "Discovery", duration: 320, synced: true, instrumental: false }, { id: 6, track: "One More Time", artist: "Daft Punk", album: "", duration: 320, synced: false, instrumental: true }, ]), r); check("requête q transmise", calls[0].url.pathname === "/api/search" && calls[0].url.searchParams.get("q") === "daft punk one more time"); mock(() => { throw new TypeError("fetch failed"); }); r = await L.searchCandidates("abc"); check("réseau en panne → UNAVAILABLE", eq(r, { ok: false, error: "UNAVAILABLE" })); route = () => json(200, []); r = await L.searchCandidates("abc"); check("UNAVAILABLE non mis en cache", r.ok && eq(r.results, [])); mock((url) => url.pathname === "/api/get/77" ? json(200, record({ id: 77, duration: 100 })) : json(404, {})); for (const bad of [0, -1, 1.5, "abc", null, {}, Number.MAX_SAFE_INTEGER + 2]) { check(`getById(${JSON.stringify(bad)}) → INVALID_PARAMS`, (await L.getById(bad)).error === "INVALID_PARAMS"); } r = await L.getById(77); check("getById garde la synchro (choix manuel)", r.ok && r.synced && r.match.id === 77 && r.songKey === null, r); check("getById accepte une chaîne numérique", (await L.getById("77")).ok); check("getById inconnu → NOT_FOUND", (await L.getById(78)).error === "NOT_FOUND"); mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {})); r = await L.findLyrics({ track: "One More Time (Official Video)", artist: "DaftPunkVEVO", duration: 320 }); check("findLyrics objet", r.ok && r.synced && r.songKey === null && calls[0].url.searchParams.get("artist_name") === "Daft Punk"); check("findLyrics invalide", (await L.findLyrics({ track: "" })).error === "INVALID_PARAMS" && (await L.findLyrics({ track: "a", artist: 3 })).error === "INVALID_PARAMS" && (await L.findLyrics({ track: "a", duration: "x" })).error === "INVALID_PARAMS" && (await L.findLyrics(["a"])).error === "INVALID_PARAMS" && (await L.findLyrics("x".repeat(201))).error === "INVALID_PARAMS"); mock((url) => url.searchParams.has("q") ? json(200, [record({ id: 90, trackName: "Time" }), record({ id: 91 })]) : json(404, {})); r = await L.findLyrics("daft punk one more time"); check("findLyrics texte libre", r.ok && r.match.track === "One More Time", r); mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {})); const text = await L.getLyrics("Daft Punk - One More Time"); check("getLyrics (compat) → texte brut", text === PLAIN, text); mock(() => json(404, {})); check("getLyrics (compat) → null si rien", (await L.getLyrics("zzz introuvable")) === null && (await L.getLyrics(null)) === null); } console.log("titres YouTube supplémentaires"); { const g = (title, author, extra = {}) => L.guessQuery({ title, author, type: "youtube", ...extra }); check("Artiste - Titre (Official Video) [4K]", eq(g("Daft Punk - Around the World (Official Video) [4K]", "Daft Punk"), { track: "Around the World", artist: "Daft Punk", duration: 0 })); check("Titre (Official Music Video) 4K sans tiret", L.cleanTitle("Blinding Lights (Official Music Video) 4K") === "Blinding Lights"); check("chaîne « - Topic » + feat. dans le titre", eq(g("Instant Crush (feat. Julian Casablancas)", "Daft Punk - Topic", { duration: 337 }), { track: "Instant Crush", artist: "Daft Punk", duration: 337 })); check("ft. en fin de titre", L.cleanTitle("Stay ft. Justin Bieber") === "Stay"); check("Titre [Lyrics] / (Audio)", L.cleanTitle("Bad Guy [Lyrics]") === "Bad Guy" && L.cleanTitle("Lose Yourself (Audio)") === "Lose Yourself"); check("« (Official Video) » + émoji", L.cleanTitle("🔥 Alors on danse (Official Video) 🔥") === "Alors on danse"); check("suffixe « - Live » conservé comme version : titre seul", eq(g("Hey Jude - Live", "The Beatles"), { track: "Hey Jude", artist: "The Beatles", duration: 0 })); check("chaîne Official + titre sans tiret", eq(g("Bohemian Rhapsody (Official Video Remastered)", "Queen Official"), { track: "Bohemian Rhapsody", artist: "Queen", duration: 0 })); } console.log("similarity / rankCandidates"); { check("identique → 1", L.similarity("One More Time", "one more time") === 1); check("accents et apostrophes ignorés", L.similarity("Je t'aime", "Je taime") === 1 && L.similarity("Éléphant", "elephant") === 1); check("espaces ignorés → 0.95", L.similarity("AC DC", "ACDC") === 0.95); check("inclusion → ≥ 0.6", L.similarity("One More Time", "One More Time Radio Edit") >= 0.6); check("rien en commun → 0", L.similarity("Hello", "Goodbye") === 0 && L.similarity("", "x") === 0); const ranked = L.rankCandidates([ record({ id: 1, trackName: "Something Else", artistName: "Nobody" }), record({ id: 2, trackName: "Daft Punk - One More Time (Official Video)" }), record({ id: 3, syncedLyrics: null }), record({ id: 4 }), ], "daft punk one more time"); check("classement : synchro propre, synchro bruitée, texte seul, hors sujet", eq(ranked.map(r => r.id), [4, 2, 3, 1]), ranked.map(r => r.id)); check("rankCandidates ne modifie pas l'entrée", (() => { const list = [record({ id: 9 })]; L.rankCandidates(list, "x"); return list.length === 1; })()); } console.log("durées de cache (horloge simulée)"); { const MIN = 60 * 1000; const HOUR = 60 * MIN; check("ttl trouvé 6 h", L.ttlForResult({ ok: true }) === 6 * HOUR); check("ttl introuvable 20 min", L.ttlForResult({ ok: false, error: "NOT_FOUND" }) === 20 * MIN); check("ttl indisponible 1 min", L.ttlForResult({ ok: false, error: "UNAVAILABLE" }) === MIN); check("ttl autre erreur / rien → 0", L.ttlForResult({ ok: false, error: "INVALID_PARAMS" }) === 0 && L.ttlForResult(null) === 0); const realNow = Date.now; // Horloge monotone : le limiteur retient l'heure du dernier démarrage, reculer le bloquerait let offset = 0; let base = 0; const at = (ms) => { offset = base + ms; }; const nextScenario = () => { base = offset + HOUR; offset = base; }; Date.now = () => realNow() + offset; try { const song = { type: "youtube", id: "ttl", title: "Daft Punk - One More Time", author: "Daft Punk", duration: 320 }; const lrclibCalls = () => calls.filter(c => c.url.host === "lrclib.net").length; mock((url) => url.pathname === "/api/get" ? json(200, record()) : json(404, {})); await L.getLyricsForSong(song); let n = lrclibCalls(); at(6 * HOUR - MIN); await L.getLyricsForSong(song); check("trouvé : encore en cache à 5 h 59", lrclibCalls() === n); at(6 * HOUR + MIN); await L.getLyricsForSong(song); check("trouvé : redemandé après 6 h", lrclibCalls() > n); nextScenario(); mock(() => json(404, {})); await L.getLyricsForSong(song); n = calls.length; at(19 * MIN); await L.getLyricsForSong(song); check("introuvable : en cache à 19 min", calls.length === n); at(21 * MIN); await L.getLyricsForSong(song); check("introuvable : redemandé après 20 min", calls.length > n); nextScenario(); mock((url) => { if (url.host === "lrclib.net") throw new TypeError("fetch failed"); return json(404, {}); }); await L.getLyricsForSong(song); n = calls.length; at(30 * 1000); await L.getLyricsForSong(song); check("indisponible : en cache 30 s", calls.length === n); at(61 * 1000); await L.getLyricsForSong(song); check("indisponible : redemandé après 1 min", calls.length > n); nextScenario(); mock(() => json(200, [record({ id: 5 })])); await L.searchCandidates("daft punk"); n = calls.length; at(9 * MIN); await L.searchCandidates("Daft Punk"); check("recherche manuelle : cache 10 min (requête normalisée)", calls.length === n); at(11 * MIN); await L.searchCandidates("daft punk"); check("recherche manuelle : redemandée après 10 min", calls.length > n); } finally { Date.now = realNow; } } globalThis.fetch = realFetch; done(); })().catch((error) => { globalThis.fetch = realFetch; console.error(error); process.exitCode = 1; });