trainlog/tests/test_web_sessions_browser.py

264 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Exercise Web Sessions through embedded production assets in real Firefox."""
import json
import os
import signal
import socket
import sqlite3
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / os.environ.get("TRAINLOG_WEB_E2E_BUILD", "build") / "tui"
GECKO = Path(
os.environ.get(
"TRAINLOG_GECKODRIVER", "/home/fy59/.cache/trainlog/web-e2e-tools/geckodriver"
)
)
EVIDENCE = ROOT / "docs" / "reviews" / "evidence"
class BrowserSessionsTest(unittest.TestCase):
def test_create_preferences_reload_and_withdraw_by_real_click(self):
if not GECKO.is_file():
self.skipTest("pinned geckodriver unavailable")
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support.ui import WebDriverWait
import websocket
with tempfile.TemporaryDirectory(prefix="trainlog-web-sessions-") as directory:
root = Path(directory)
xdg = root / "data"
config = root / "config"
xdg.mkdir()
config.mkdir()
with socket.socket() as reserved:
reserved.bind(("127.0.0.1", 0))
port = reserved.getsockname()[1]
environment = os.environ.copy()
environment.update(
{
"XDG_DATA_HOME": str(xdg),
"XDG_CONFIG_HOME": str(config),
"XDG_CACHE_HOME": str(root / "cache"),
}
)
server = subprocess.Popen(
[str(BUILD / "trainlog"), "--web", "--port", str(port)],
env=environment,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
driver = None
bidi = None
try:
for _ in range(200):
with socket.socket() as probe:
if probe.connect_ex(("127.0.0.1", port)) == 0:
break
time.sleep(0.025)
options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.set_capability("webSocketUrl", True)
driver = webdriver.Firefox(options=options, service=Service(str(GECKO)))
wait = WebDriverWait(driver, 15)
bidi = websocket.create_connection(
driver.capabilities["webSocketUrl"], suppress_origin=True
)
command_id = 0
def bidi_command(method: str, parameters: dict):
nonlocal command_id
command_id += 1
bidi.send(json.dumps({
"id": command_id,
"method": method,
"params": parameters,
}))
while True:
response = json.loads(bidi.recv())
if response.get("id") != command_id:
continue
if response.get("type") != "success":
raise AssertionError(response)
return response["result"]
contexts = bidi_command("browsingContext.getTree", {})["contexts"]
self.assertEqual(len(contexts), 1)
context = contexts[0]["context"]
def set_viewport(width: int, height: int):
bidi_command(
"browsingContext.setViewport",
{
"context": context,
"viewport": {"width": width, "height": height},
"devicePixelRatio": 1,
},
)
wait.until(
lambda current: current.execute_script(
"return innerWidth===arguments[0]&&innerHeight===arguments[1]",
width,
height,
)
)
set_viewport(1440, 1000)
driver.get(f"http://127.0.0.1:{port}/seances")
wait.until(lambda current: "Aucun résultat" in current.page_source)
EVIDENCE.mkdir(parents=True, exist_ok=True)
self.assertTrue(driver.save_screenshot(
str(EVIDENCE / "web-sessions-list-desktop.png")
))
driver.find_element(By.XPATH, "//button[normalize-space()='Nouvelle séance']").click()
wait.until(lambda current: "Nouvelle séance" in current.page_source)
title = driver.find_element(By.XPATH, "//label[normalize-space()='Titre']/input")
title.send_keys("Séance navigateur éphémère")
planned = driver.find_element(
By.XPATH, "//label[normalize-space()='Date planifiée']/input"
)
driver.execute_script(
"const setter=Object.getOwnPropertyDescriptor("
"HTMLInputElement.prototype,'value').set;"
"setter.call(arguments[0],'2026-09-18');"
"arguments[0].dispatchEvent(new Event('input',{bubbles:true}));"
"arguments[0].dispatchEvent(new Event('change',{bubbles:true}));",
planned,
)
catalog_choice = wait.until(
lambda current: current.find_element(
By.CSS_SELECTOR, ".catalog-results button"
)
)
catalog_choice.click()
set_viewport(390, 844)
self.assertTrue(driver.save_screenshot(
str(EVIDENCE / "web-sessions-editor-mobile.png")
))
save = driver.find_element(
By.XPATH,
"//button[normalize-space()='Marquer prête / Préparer pour Android']",
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", save)
save.click()
wait.until(lambda current: "PRÉPARATION MANUELLE" in current.page_source)
driver.refresh()
wait.until(lambda current: "Séance navigateur éphémère" in current.page_source)
set_viewport(1440, 1000)
self.assertTrue(driver.save_screenshot(
str(EVIDENCE / "web-sessions-detail-desktop.png")
))
self.assertIn("18/09/2026", driver.page_source)
driver.find_element(
By.XPATH, "//button[normalize-space()='Paramètres d’affichage']"
).click()
wait.until(lambda current: "Format des dates" in current.page_source)
driver.find_element(By.CSS_SELECTOR, "input[value='iso']").click()
wait.until(
lambda current: current.find_element(
By.CSS_SELECTOR, "main time"
).text == "2026-09-18"
)
driver.refresh()
wait.until(
lambda current: current.find_element(
By.CSS_SELECTOR, "main time"
).text == "2026-09-18"
)
preferences = (
config / "trainlog" / "web" / "preferences-v1.json"
).read_text(encoding="utf-8")
self.assertIn('"date_format":"iso"', preferences)
driver.find_element(
By.XPATH, "//button[normalize-space()='Supprimer la préparation']"
).click()
confirmation = wait.until(
lambda current: current.find_element(By.CSS_SELECTOR, "[role='alertdialog']")
)
self.assertIn("Séance navigateur éphémère", confirmation.text)
set_viewport(390, 844)
self.assertTrue(driver.save_screenshot(
str(EVIDENCE / "web-sessions-delete-confirmation-mobile.png")
))
confirmation.find_element(
By.XPATH, ".//button[normalize-space()='Supprimer la préparation']"
).click()
wait.until(
lambda current: "annulation Android à synchroniser" in current.page_source
)
set_viewport(1440, 1000)
self.assertTrue(driver.save_screenshot(
str(EVIDENCE / "web-sessions-withdrawn-detail-desktop.png")
))
driver.refresh()
wait.until(lambda current: "Supprimée" in current.page_source)
self.assertNotIn(
"Supprimer la préparation",
driver.find_element(By.CSS_SELECTOR, "main").text,
)
database_path = xdg / "trainlog" / "trainlog.db"
with sqlite3.connect(database_path) as database:
row = database.execute(
"SELECT r.title, p.editing_state, p.withdrawn_at "
"FROM session_preparations p "
"JOIN session_preparation_revisions r "
"ON r.revision_id = p.current_revision_id"
).fetchone()
entry_count = database.execute(
"SELECT COUNT(*) FROM session_preparation_entries"
).fetchone()[0]
withdrawal_count = database.execute(
"SELECT COUNT(*) FROM session_preparation_withdrawals"
).fetchone()[0]
delivery_count = database.execute(
"SELECT COUNT(*) FROM session_preparation_deliveries"
).fetchone()[0]
self.assertEqual(row[:2], ("Séance navigateur éphémère", "ready"))
self.assertIsNotNone(row[2])
self.assertEqual(entry_count, 1)
self.assertEqual(delivery_count, 1)
self.assertEqual(withdrawal_count, 1)
export_path = root / "session-preparations-v2.json"
subprocess.run(
[
sys.executable,
str(ROOT / "tools" / "export_session_preparations.py"),
str(export_path),
"--database",
str(database_path),
],
check=True,
capture_output=True,
text=True,
)
exported = json.loads(export_path.read_text(encoding="utf-8"))
self.assertEqual(exported["deliveries"], [])
self.assertEqual(len(exported["withdrawals"]), 1)
self.assertEqual(len(exported["withdrawals"][0]["deliveries"]), 1)
finally:
if bidi is not None:
bidi.close()
if driver is not None:
driver.quit()
os.killpg(server.pid, signal.SIGTERM)
server.wait(timeout=10)
if __name__ == "__main__":
unittest.main()