QuantumLearning/tests/test_browser_ux.py

171 lines
5.7 KiB
Python

from __future__ import annotations
import os
import socket
import subprocess
import time
from urllib.error import URLError
from urllib.request import urlopen
import pytest
from quantum_learning.config import project_root
try:
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright
except ImportError: # pragma: no cover - test environment specific
sync_playwright = None
PlaywrightTimeoutError = RuntimeError
def _find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind(("127.0.0.1", 0))
except PermissionError:
pytest.skip("Browser UX tests need localhost ports, which this sandbox blocks.")
return int(sock.getsockname()[1])
def _wait_for_server(url: str, process: subprocess.Popen[str], timeout_seconds: int = 90) -> None:
deadline = time.time() + timeout_seconds
while time.time() < deadline:
if process.poll() is not None:
stdout = process.stdout.read() if process.stdout is not None else ""
raise RuntimeError(f"Jupyter server exited early.\n{stdout}")
try:
with urlopen(url, timeout=2):
return
except URLError:
time.sleep(1)
raise TimeoutError(f"Timed out waiting for JupyterLab at {url}")
def _wait_for_notebook(browser_page, notebook_filename: str, heading: str) -> None:
browser_page.locator(".lm-TabBar-tabLabel", has_text=notebook_filename).last.wait_for(
timeout=60000
)
browser_page.locator(".jp-RenderedHTMLCommon h1", has_text=heading).last.wait_for(
timeout=60000
)
@pytest.fixture(scope="module")
def jupyter_lab_server():
if sync_playwright is None:
pytest.skip("Playwright is not installed in this environment.")
root = project_root()
browser_root = root / ".playwright-browsers"
if not browser_root.exists():
pytest.skip("Project-local Playwright browsers are not installed.")
port = _find_free_port()
token = "quantum-learning-browser-test"
runtime_root = root / ".tmp_test_artifacts" / "browser_jupyter"
config_dir = runtime_root / "config"
data_dir = runtime_root / "data"
runtime_dir = runtime_root / "runtime"
ipython_dir = runtime_root / "ipython"
mpl_dir = runtime_root / "matplotlib"
for path in (config_dir, data_dir, runtime_dir, ipython_dir, mpl_dir):
path.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env.update(
{
"PATH": f"{root / '.venv' / 'bin'}:/usr/bin:/bin:/usr/sbin:/sbin",
"JUPYTER_CONFIG_DIR": str(config_dir),
"JUPYTER_DATA_DIR": str(data_dir),
"JUPYTER_RUNTIME_DIR": str(runtime_dir),
"IPYTHONDIR": str(ipython_dir),
"MPLCONFIGDIR": str(mpl_dir),
"PLAYWRIGHT_BROWSERS_PATH": str(browser_root),
}
)
command = [
str(root / ".venv" / "bin" / "jupyter"),
"lab",
"--no-browser",
"--ip=127.0.0.1",
f"--port={port}",
f"--IdentityProvider.token={token}",
f"--ServerApp.root_dir={root}",
]
process = subprocess.Popen(
command,
cwd=root,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
base_url = f"http://127.0.0.1:{port}"
_wait_for_server(f"{base_url}/lab?token={token}", process)
try:
yield {"base_url": base_url, "token": token, "browser_root": browser_root}
finally:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired: # pragma: no cover - cleanup path
process.kill()
@pytest.fixture()
def browser_page(jupyter_lab_server):
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(jupyter_lab_server["browser_root"])
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()
try:
yield page
finally:
browser.close()
@pytest.mark.browser
def test_course_blueprint_loads_in_jupyterlab(browser_page, jupyter_lab_server):
url = (
f"{jupyter_lab_server['base_url']}/lab/tree/notebooks/COURSE_BLUEPRINT.ipynb"
f"?token={jupyter_lab_server['token']}"
)
browser_page.goto(url, wait_until="networkidle")
_wait_for_notebook(browser_page, "COURSE_BLUEPRINT.ipynb", "Course Blueprint")
assert browser_page.locator(".jp-Notebook .jp-RenderedHTMLCommon", has_text="The Four Technical Bands").first.is_visible()
@pytest.mark.browser
def test_capstone_studio_runs_and_renders_controls(browser_page, jupyter_lab_server):
url = (
f"{jupyter_lab_server['base_url']}/lab/tree/notebooks/professional/"
"module_04_capstone_design_review"
f"?token={jupyter_lab_server['token']}"
)
browser_page.goto(url, wait_until="networkidle")
browser_page.locator(".jp-DirListing-itemText", has_text="studio.ipynb").first.wait_for(
timeout=60000
)
browser_page.locator(".jp-DirListing-itemText", has_text="studio.ipynb").first.dblclick()
_wait_for_notebook(
browser_page,
"studio.ipynb",
"Capstone Circuit Design Review Studio",
)
browser_page.locator("text=Run").first.click()
browser_page.locator("text=Run All Cells").first.click()
try:
browser_page.get_by_role("button", name="Select").click(timeout=5000)
except PlaywrightTimeoutError:
pass
browser_page.get_by_role("button", name="Render circuit").first.wait_for(timeout=120000)
browser_page.get_by_role("button", name="Check answer").first.wait_for(timeout=120000)
browser_page.locator("text=Evidence Checklist").first.wait_for(timeout=120000)