feat: real API-driven search pagination
This commit is contained in:
@@ -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)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour searchVideos() (includes/config.php).
|
||||||
|
*
|
||||||
|
* Aucun appel réseau : des réponses API factices sont pré-déposées dans le
|
||||||
|
* cache (callPeerTubeApiCached consulte le cache avant tout appel cURL, qui
|
||||||
|
* de toute façon échouerait avec PEERTUBE_URL = http://127.0.0.1). Le
|
||||||
|
* répertoire de cache de l'instance globale est redirigé vers un dossier
|
||||||
|
* temporaire via réflexion pour ne jamais toucher au cache réel (cache/api).
|
||||||
|
*/
|
||||||
|
|
||||||
|
$cache = $GLOBALS['simple_api_cache'];
|
||||||
|
$cacheDirProperty = new ReflectionProperty(SimpleAPICache::class, 'cacheDir');
|
||||||
|
$cacheDirProperty->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);
|
||||||
Reference in New Issue
Block a user