test(e2e): add Playwright tests for homepage, video page and load-more

This commit is contained in:
2026-07-26 20:22:20 +04:00
parent 54a4368ea8
commit 606175d718
4 changed files with 418 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
"""
Fixtures pytest pour les tests E2E : serveur PHP local + page Playwright.
Le serveur de développement PHP (`php -S`) est démarré une fois par session
de test sur 127.0.0.1:8000 (ou sur un port libre si 8000 est déjà pris).
Les appels aux API distantes (PeerTube, Castopod) passent par le vrai réseau :
le premier chargement remplit le cache du site (cache/api), ce qui peut
prendre un certain temps.
Lancement : /tmp/test-venv/bin/python -m pytest tests/e2e/
"""
import os
import re
import socket
import subprocess
import tempfile
import time
import urllib.request
from pathlib import Path
import pytest
from playwright.sync_api import sync_playwright
PROJECT_ROOT = Path(__file__).resolve().parents[2]
SERVER_HOST = "127.0.0.1"
DEFAULT_PORT = 8000
STARTUP_TIMEOUT = 30 # secondes pour que php -S accepte des connexions
WARMUP_TIMEOUT = 90 # premier hit : remplit les caches API (Castopod/PeerTube)
PAGE_TIMEOUT_MS = 60000
def _port_libre(port):
"""Retourne True si aucun service n'écoute sur le port donné."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex((SERVER_HOST, port)) != 0
def _trouver_port():
"""Utilise le port 8000 si possible, sinon un port libre quelconque."""
if _port_libre(DEFAULT_PORT):
return DEFAULT_PORT
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((SERVER_HOST, 0))
return s.getsockname()[1]
def lire_constante_config(nom, defaut=None):
"""
Lit une constante define('NOM', 'valeur') dans la configuration PHP.
Cherche dans config.default.php puis dans config.local.php (qui a le
dernier mot, comme dans includes/config.php). Ne gère que les valeurs
chaînes simples — suffisant pour SITE_NAME, PEERTUBE_URL, etc.
"""
valeur = defaut
for fichier in ("config.default.php", "config.local.php"):
chemin = PROJECT_ROOT / "includes" / fichier
if not chemin.is_file():
continue
texte = chemin.read_text(encoding="utf-8")
motif = r"define\(\s*'" + re.escape(nom) + r"'\s*,\s*'((?:[^'\\]|\\.)*)'\s*\)"
m = re.search(motif, texte)
if m:
valeur = m.group(1).replace("\\'", "'").replace("\\\\", "\\")
return valeur
@pytest.fixture(scope="session")
def base_url():
"""Démarre `php -S` à la racine du projet et fournit l'URL de base."""
port = _trouver_port()
url = f"http://{SERVER_HOST}:{port}"
log = tempfile.NamedTemporaryFile(
mode="w+", prefix="php-e2e-", suffix=".log", delete=False
)
# PHP_CLI_SERVER_WORKERS : le serveur intégré est mono-thread par défaut,
# ce qui bloque quand le navigateur demande plusieurs PHP en parallèle.
env = dict(os.environ, PHP_CLI_SERVER_WORKERS="4")
proc = subprocess.Popen(
["php", "-S", f"{SERVER_HOST}:{port}", "-t", str(PROJECT_ROOT)],
cwd=str(PROJECT_ROOT),
stdout=log,
stderr=subprocess.STDOUT,
env=env,
)
# Attendre que le serveur accepte des connexions
deadline = time.monotonic() + STARTUP_TIMEOUT
pret = False
while time.monotonic() < deadline:
if proc.poll() is not None:
log.seek(0)
raise RuntimeError(
"Le serveur PHP s'est arrêté au démarrage :\n" + log.read()
)
try:
with socket.create_connection((SERVER_HOST, port), timeout=1):
pret = True
break
except OSError:
time.sleep(0.2)
if not pret:
proc.terminate()
raise RuntimeError(
f"Le serveur PHP ne répond pas après {STARTUP_TIMEOUT} s"
)
# Pré-chauffe les caches (initCategories + appels API distants)
try:
with urllib.request.urlopen(url + "/index.php", timeout=WARMUP_TIMEOUT) as rep:
if rep.status != 200:
raise RuntimeError(f"index.php a répondu HTTP {rep.status}")
except Exception:
proc.terminate()
log.seek(0)
raise RuntimeError(
"Impossible de charger la page d'accueil :\n" + log.read()
)
yield url
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
log.close()
@pytest.fixture(scope="session")
def navigateur():
"""Instance Chromium partagée pour toute la session de test."""
with sync_playwright() as p:
browser = p.chromium.launch()
yield browser
browser.close()
@pytest.fixture()
def page(navigateur):
"""Nouvelle page vierge pour chaque test (contexte isolé)."""
context = navigateur.new_context()
context.set_default_timeout(PAGE_TIMEOUT_MS)
pg = context.new_page()
yield pg
context.close()
@pytest.fixture(scope="session")
def site_name():
"""Valeur de la constante SITE_NAME de la configuration du site."""
return lire_constante_config("SITE_NAME")
@pytest.fixture(scope="session")
def peertube_url():
"""Valeur de la constante PEERTUBE_URL de la configuration du site."""
return lire_constante_config("PEERTUBE_URL")