391 lines
26 KiB
JavaScript
391 lines
26 KiB
JavaScript
// 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;
|
||
});
|