Nouveau formulaire "Recherche YouTube" (mots-cles + limite optionnelle), branche sur la zone de resultats existante (miniatures, bouton "Ajouter" par ligne ; pas de "Tout ajouter", comme demande). - Avec une limite : tous les resultats d'un coup (une seule page). - Sans limite : defilement infini. Un element sentinelle en fin de liste, observe par un IntersectionObserver (racine = la liste, marge 300 px), charge la page suivante ; il est rearme apres chaque page, affiche chargement / erreur avec "Reessayer" / "Fin des resultats", et une page en echec ne fait pas perdre les precedentes. Serveur (GET/POST/DELETE /api/search) : une "session" = un seul yt-dlp (--flat-playlist --lazy-playlist -j -- ytsearch1000:<requete>) servi page par page depuis un tampon. Pourquoi pas un appel yt-dlp par page - mesure avec le vrai yt-dlp : l'ordre des resultats de YouTube n'est pas deterministe meme pour deux appels identiques (5/20 aux memes positions, 69 % de recouvrement sur 100), donc paginer en relancant la recherche donnait 15 a 30 % de doublons des la 3e-4e page et des trous ; YouTube s'arrete vers ~590 resultats ; ~7 % de doublons internes (dedoublonnes a l'ingestion) ; sans --lazy-playlist yt-dlp n'imprime rien avant la fin (~23 s), avec il emet au fil des pages (20 en ~2,3 s). Le parcours n'avance qu'a la demande : quand le tampon a 20 resultats d'avance, la lecture de stdout est suspendue (nouvelle option onSpawn de runYtDlp) ; le tube se remplit et yt-dlp bloque (mesure : plus aucune ligne pendant 10 s de pause). On n'aspire donc pas ~590 resultats, et ~30 requetes vers YouTube, quand l'utilisateur ne defile pas. Cycle de vie : 4 sessions simultanees max (la moins recemment utilisee est fermee), expiration a 10 min d'inactivite / 30 min de vie, fermeture des que la derniere page est servie, sur DELETE (liste remplacee cote client), au pagehide du client, et a l'arret du serveur. Requete validee (1 a 200 caracteres, sans caractere de controle) et transmise en un seul argument ytsearchN:<requete>, jamais interpretable comme une option. Mise en page : ce troisieme formulaire laissait a la liste ~104 px (une ligne) a 1366x650. Une liste non vide garde min-height 13rem et la colonne du formulaire defile au lieu de rogner (la page ne defile toujours pas). Verifie : contre le vrai yt-dlp (page tamponnee en 4 ms, page 200 apres 12 s d'inactivite en ~4 s : la suspension tient ; 29 pages -> 576 videos, 576 uniques ; processus fermes a la fin / sur DELETE / au plafond) et dans le navigateur (580 resultats sans doublon jusqu'a "Fin des resultats", requetes de page uniques malgre deux signaux simultanes, echec + reprise, session expiree, remplacement de la recherche, mode limite, ajout d'un resultat a la file, pagehide, playlist et chaine en non-regression). Le volet IntersectionObserver a ete pilote par un observateur factice : le panneau de navigation n'affichait plus de frames (rAF/IO natifs muets). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
32 lines
1.3 KiB
TypeScript
32 lines
1.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
InvalidSearchQueryError,
|
||
MAX_QUERY_LENGTH,
|
||
assertValidSearchQuery,
|
||
} from '../src/services/searchQuery.js';
|
||
|
||
describe('assertValidSearchQuery', () => {
|
||
it.each([
|
||
['une requête simple', 'speed climbing', 'speed climbing'],
|
||
['des espaces superflus', ' speed climbing ', 'speed climbing'],
|
||
['des accents et de l’unicode', 'escalade été 日本語', 'escalade été 日本語'],
|
||
['des symboles courants', 'c++ "tutoriel" -débutant #1', 'c++ "tutoriel" -débutant #1'],
|
||
['un tiret en tête (jamais pris pour une option)', '--exec=rm', '--exec=rm'],
|
||
['une longueur maximale exacte', 'a'.repeat(MAX_QUERY_LENGTH), 'a'.repeat(MAX_QUERY_LENGTH)],
|
||
])('accepte %s', (_label, input, expected) => {
|
||
expect(assertValidSearchQuery(input)).toBe(expected);
|
||
});
|
||
|
||
it.each([
|
||
['vide', ''],
|
||
['faite d’espaces', ' '],
|
||
['trop longue', 'a'.repeat(MAX_QUERY_LENGTH + 1)],
|
||
['avec un retour à la ligne', 'speed\nclimbing'],
|
||
['avec un retour chariot', 'speed\rclimbing'],
|
||
['avec une tabulation', 'speed\tclimbing'],
|
||
['avec un caractère NUL', 'speed |