diff --git a/tests/e2e/test_search_pagination.py b/tests/e2e/test_search_pagination.py new file mode 100644 index 0000000..96b8ab0 --- /dev/null +++ b/tests/e2e/test_search_pagination.py @@ -0,0 +1,125 @@ +"""Tests E2E de la pagination de la recherche (recherche.php). + +La pagination est déléguée à l'API PeerTube (paramètre start) et le total +affiché est le total réel renvoyé par l'API — et non le nombre de vidéos +réellement présentes sur la page. +""" +import json +import math +import re +import urllib.parse +import urllib.request + +import pytest + +VIDEOS_PAR_PAGE = 12 # VIDEOS_PER_PAGE (includes/config.default.php) +TERMES_CANDIDATS = ["a", "e", "video", "les"] + + +def _total_api(peertube_url, terme): + """Total d'une recherche plein texte côté API PeerTube.""" + params = urllib.parse.urlencode({ + "search": terme, + "isLocal": "true", + "count": 1, + }) + url = f"{peertube_url}/api/v1/search/videos?{params}" + with urllib.request.urlopen(url, timeout=30) as rep: + return int(json.load(rep).get("total", 0)) + + +def _nombre_resultats(page): + """Extrait le total affiché dans le bandeau des résultats.""" + texte = page.locator(".search-results-count p").inner_text() + m = re.search(r"(\d+)", texte) + assert m, f"Total introuvable dans le bandeau : {texte!r}" + return int(m.group(1)) + + +def _ids_cartes(page): + """Identifiants des vidéos affichées dans la grille de résultats.""" + cartes = page.locator(".video-grid .video-card") + return { + cartes.nth(i).get_attribute("data-video-id") + for i in range(cartes.count()) + } + + +@pytest.fixture(scope="session") +def terme_paginable(peertube_url): + """Un terme de recherche couvrant au moins deux pages de résultats.""" + for terme in TERMES_CANDIDATS: + try: + if _total_api(peertube_url, terme) > VIDEOS_PAR_PAGE: + return terme + except Exception: + continue + pytest.skip("Aucun terme avec plus d'une page de résultats (réseau ?)") + + +def test_recherche_affiche_total_reel(page, base_url, peertube_url, terme_paginable): + """Le total affiché est le total réel de l'API, pas le contenu de la page.""" + total_api = _total_api(peertube_url, terme_paginable) + page.goto( + f"{base_url}/recherche.php?q={urllib.parse.quote(terme_paginable)}", + wait_until="domcontentloaded", + ) + + cartes = page.locator(".video-grid .video-card") + if cartes.count() == 0: + pytest.skip("Aucune vidéo reçue de l'API PeerTube (réseau indisponible ?)") + + total_affiche = _nombre_resultats(page) + assert total_affiche == total_api + # La première page ne contient qu'une page de résultats, pas le total + assert total_affiche > cartes.count() + assert cartes.count() == min(VIDEOS_PAR_PAGE, total_affiche) + + # La barre de pagination reflète le total réel + assert page.locator(".pagination").count() == 1 + assert page.locator(".page-number.current").inner_text() == "1" + numeros = [ + int(page.locator(".page-number").nth(i).inner_text()) + for i in range(page.locator(".page-number").count()) + ] + assert max(numeros) == math.ceil(total_affiche / VIDEOS_PAR_PAGE) + + +def test_recherche_navigation_page_2(page, base_url, terme_paginable): + """Le lien « Suivant » mène à la page 2, avec des vidéos différentes.""" + page.goto( + f"{base_url}/recherche.php?q={urllib.parse.quote(terme_paginable)}", + wait_until="domcontentloaded", + ) + ids_page1 = _ids_cartes(page) + if not ids_page1: + pytest.skip("Aucune vidéo reçue de l'API PeerTube (réseau indisponible ?)") + + page.locator(".page-link.next").click() + page.wait_for_url(re.compile(r"[?&]page=2(&|$)")) + + assert page.locator(".page-number.current").inner_text() == "2" + assert page.locator(".page-link.prev").count() == 1 + + ids_page2 = _ids_cartes(page) + assert ids_page2, "La page 2 ne contient aucune vidéo" + assert ids_page2.isdisjoint(ids_page1), "Les pages 1 et 2 partagent des vidéos" + + +def test_recherche_page_hors_limites(page, base_url, terme_paginable): + """Une page au-delà de la dernière retombe sur une page valide.""" + page.goto( + f"{base_url}/recherche.php?q={urllib.parse.quote(terme_paginable)}&page=99999999", + wait_until="domcontentloaded", + ) + + cartes = page.locator(".video-grid .video-card") + if cartes.count() == 0: + pytest.skip("Aucune vidéo reçue de l'API PeerTube (réseau indisponible ?)") + + # La page se rabat sur une page valide (jamais de grille vide ni d'erreur) + total_affiche = _nombre_resultats(page) + assert total_affiche > 0 + assert page.locator(".page-number.current").count() == 1 + page_courante = int(page.locator(".page-number.current").inner_text()) + assert 1 <= page_courante <= math.ceil(total_affiche / VIDEOS_PAR_PAGE) diff --git a/tests/php/search-test.php b/tests/php/search-test.php new file mode 100644 index 0000000..449c7ef --- /dev/null +++ b/tests/php/search-test.php @@ -0,0 +1,127 @@ +setAccessible(true); + +$tmpDir = sys_get_temp_dir() . '/annu-kute-search-test-' . getmypid(); +if (!is_dir($tmpDir)) { + mkdir($tmpDir, 0755, true); +} +$cacheDirProperty->setValue($cache, $tmpDir); + +/** + * Fabrique une entrée vidéo brute minimale au format API PeerTube + */ +function fakeApiVideo($uuid, $name = 'Vidéo de test') { + return [ + 'uuid' => $uuid, + 'name' => $name, + 'duration' => 42, + 'publishedAt' => '2024-01-01T00:00:00.000Z' + ]; +} + +// --- Requête vide ----------------------------------------------------------- + +$total = null; +$videos = searchVideos('', 12, 0, $total); +assertEquals([], $videos, 'une requête vide retourne un tableau vide'); +assertEquals(0, $total, 'une requête vide donne un total de 0'); + +// --- Paramètres de la requête API (identiques à ceux de searchVideos) ------- + +$paramsPage1 = [ + 'search' => 'assemblee', + 'count' => 12, + 'start' => 0, + 'isLocal' => true, + 'sort' => '-publishedAt' +]; +$paramsPage2 = $paramsPage1; +$paramsPage2['start'] = 12; + +// Deux pages factices : total 25, 12 vidéos en page 1, 13 en page 2 +$dataPage1 = []; +for ($i = 1; $i <= 12; $i++) { + $dataPage1[] = fakeApiVideo('uuid-page1-' . $i); +} +$dataPage2 = []; +for ($i = 1; $i <= 13; $i++) { + $dataPage2[] = fakeApiVideo('uuid-page2-' . $i); +} +$cache->set('search/videos', $paramsPage1, ['total' => 25, 'data' => $dataPage1]); +$cache->set('search/videos', $paramsPage2, ['total' => 25, 'data' => $dataPage2]); + +// --- Total réel renvoyé par l'API ------------------------------------------- + +$total = null; +$videos = searchVideos('assemblee', 12, 0, $total); +assertEquals(12, count($videos), 'la page 1 retourne les 12 vidéos de la réponse'); +assertEquals(25, $total, 'le total provient de l\'API (25), pas du nombre de vidéos reçues'); +assertEquals('uuid-page1-1', $videos[0]['id'], 'les vidéos sont formatées par formatVideosData'); + +// --- Le paramètre start pilote la pagination --------------------------------- + +$total = null; +$videos = searchVideos('assemblee', 12, 12, $total); +assertEquals(13, count($videos), 'start=12 retourne la deuxième page (13 vidéos)'); +assertEquals('uuid-page2-1', $videos[0]['id'], 'start=12 interroge bien l\'API avec un décalage'); +assertEquals(25, $total, 'le total reste le même sur la deuxième page'); + +// --- Recherche par hashtag (endpoint videos) --------------------------------- + +$paramsTag = [ + 'tagsOneOf' => 'reunion', + 'count' => 12, + 'start' => 0, + 'isLocal' => true, + 'sort' => '-publishedAt' +]; +$cache->set('videos', $paramsTag, ['total' => 3, 'data' => [ + fakeApiVideo('uuid-tag-1'), + fakeApiVideo('uuid-tag-2'), + fakeApiVideo('uuid-tag-3') +]]); + +$total = null; +$videos = searchVideos('#reunion', 12, 0, $total); +assertEquals(3, count($videos), 'la recherche par hashtag retourne les vidéos du tag'); +assertEquals(3, $total, 'la recherche par hashtag renseigne aussi le total'); + +// --- Réponse sans clé total : repli sur le nombre de vidéos reçues ---------- + +$paramsSansTotal = $paramsPage1; +$paramsSansTotal['search'] = 'sanstotal'; +$cache->set('search/videos', $paramsSansTotal, ['data' => [ + fakeApiVideo('uuid-st-1'), + fakeApiVideo('uuid-st-2') +]]); + +$total = null; +$videos = searchVideos('sanstotal', 12, 0, $total); +assertEquals(2, count($videos), 'réponse sans total : les vidéos sont retournées'); +assertEquals(2, $total, 'réponse sans total : repli sur le nombre de vidéos reçues'); + +// --- Aucune entrée en cache et réseau bloqué : résultat vide ----------------- + +$total = null; +$videos = searchVideos('jamaisencache', 12, 0, $total); +assertEquals([], $videos, 'sans cache ni réseau, la recherche retourne un tableau vide'); +assertEquals(0, $total, 'sans cache ni réseau, le total vaut 0'); + +// --- Nettoyage du dossier temporaire ---------------------------------------- + +foreach (glob($tmpDir . '/cache_*.json') as $file) { + unlink($file); +} +rmdir($tmpDir);