mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-05-16 21:10:07 +00:00
171 lines
5.7 KiB
Python
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)
|