Spaces:
Sleeping
Sleeping
| """Browser end-to-end test: drive the real frontend in headless Chromium | |
| against the live gr.Server app with the tiny offline model. | |
| Run directly: .venv/bin/python tests/test_browser_e2e.py | |
| Screenshots land in tests/screenshots/. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from conftest import build_byte_tokenizer, build_tiny_model # noqa: E402 | |
| PORT = 7896 | |
| BASE = f"http://127.0.0.1:{PORT}" | |
| SHOTS = Path(__file__).parent / "screenshots" | |
| def seg_check(locator) -> None: | |
| """Select a segmented-control option (its radio input is visually hidden).""" | |
| locator.evaluate("el => { el.checked = true; el.dispatchEvent(new Event('change')); }") | |
| def main() -> None: | |
| from playwright.sync_api import sync_playwright | |
| from miru_tracer.core.model_manager import ModelManager | |
| ModelManager._model = build_tiny_model() | |
| ModelManager._tokenizer = build_byte_tokenizer() | |
| ModelManager._device = "cpu" | |
| ModelManager._model_name = "tiny/test-model" | |
| import app as jacobina_app | |
| jacobina_app.server.launch( | |
| prevent_thread_lock=True, server_name="127.0.0.1", server_port=PORT, quiet=True | |
| ) | |
| SHOTS.mkdir(exist_ok=True) | |
| console_errors: list[str] = [] | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch(headless=True) | |
| page = browser.new_page(viewport={"width": 1500, "height": 1000}) | |
| page.on( | |
| "console", | |
| lambda msg: console_errors.append(msg.text) if msg.type == "error" else None, | |
| ) | |
| page.on("pageerror", lambda err: console_errors.append(str(err))) | |
| page.goto(BASE, wait_until="networkidle") | |
| assert "Jacobina" in page.title() | |
| # ---- Model view (default): status restored on load | |
| page.wait_for_selector(".model-pill.is-loaded", timeout=15000) | |
| assert "tiny/test-model" in page.text_content(".model-pill") | |
| assert "tiny/test-model" in page.inner_text("#view-model") | |
| page.screenshot(path=str(SHOTS / "01-model.png"), full_page=True) | |
| print("PASS model view (status restore)") | |
| # ---- Tokens view | |
| page.click("text=Tokens") | |
| page.fill("#view-tokens textarea", "Hello Jacobina") | |
| page.click("#view-tokens button:has-text('Tokenize')") | |
| page.wait_for_selector("#view-tokens tbody tr", timeout=10000) | |
| rows = page.locator("#view-tokens .candidates-scroll tbody tr").count() | |
| assert rows > 3, f"expected token rows, got {rows}" | |
| page.fill("#view-tokens input[type=number]", "72") | |
| page.click("#view-tokens button:has-text('Lookup token')") | |
| page.wait_for_selector("#view-tokens td:text-is('ID')", timeout=10000) | |
| page.screenshot(path=str(SHOTS / "02-tokens.png"), full_page=True) | |
| print(f"PASS tokens view ({rows} rows + lookup)") | |
| # ---- Interactive view | |
| page.click("text=Interactive") | |
| page.click("#view-interactive button:has-text('Initialize')") | |
| page.wait_for_selector("#view-interactive tbody tr", timeout=20000) | |
| candidates = page.locator("#view-interactive tbody tr").count() | |
| assert candidates >= 10, f"expected 10 candidates, got {candidates}" | |
| assert page.locator("#view-interactive tr.is-preview").count() == 1 | |
| page.click("#view-interactive button:has-text('Next step')") | |
| page.wait_for_selector("#view-interactive .status-pane:has-text('Step 1 complete')", timeout=15000) | |
| step_shown = page.text_content("#view-interactive .pill-value") | |
| assert step_shown.strip() == "1", f"step display: {step_shown!r}" | |
| page.click("#view-interactive button:has-text('Step back')") | |
| page.wait_for_selector("#view-interactive .status-pane:has-text('Undone last step')", timeout=15000) | |
| page.click("#view-interactive button:has-text('Run')") | |
| page.wait_for_selector("#view-interactive .status-pane:has-text('Total steps: 10')", timeout=60000) | |
| # layer lens panel | |
| page.click("#view-interactive summary:has-text('Layer lens')") | |
| page.click("#view-interactive button:has-text('Refresh lens')") | |
| page.wait_for_selector("#view-interactive .plot-host .plotly", timeout=20000) | |
| page.screenshot(path=str(SHOTS / "03-interactive.png"), full_page=True) | |
| print("PASS interactive view (init/step/undo/run/lens plot)") | |
| # ---- Generate view | |
| page.click("text=Generate") | |
| page.fill("#view-generate input[type=number]", "8") # max tokens | |
| page.click("#view-generate summary:has-text('Logging & visualization')") | |
| # uncheck stop at EOS so step count is deterministic | |
| page.uncheck("#view-generate .check-field:has-text('Stop at EOS') input") | |
| page.click("#view-generate button:has-text('Generate')") | |
| page.wait_for_selector("#view-generate .status-pane:has-text('Done — 8 steps')", timeout=60000) | |
| assert page.locator("#view-generate .plot-host .plotly").count() == 2 | |
| continue_btn = page.locator("#view-generate button:has-text('Continue')") | |
| assert continue_btn.is_enabled(), "Continue should be enabled after a run" | |
| # editing the prompt must invalidate Continue | |
| page.fill("#view-generate .prompt-editor textarea", "Changed prompt") | |
| page.wait_for_timeout(200) | |
| assert not continue_btn.is_enabled(), "Continue must disable when inputs change" | |
| page.screenshot(path=str(SHOTS / "04-generate.png"), full_page=True) | |
| print("PASS generate view (stream/plots/continue-invalidation)") | |
| # ---- Lens view | |
| page.click("nav >> text=Lens") | |
| # add an intervention first (steer ' the', logit basis, layer 0) | |
| iv_panel = page.locator("#view-lens .panel-details", has=page.locator("summary:has-text('Interventions')")) | |
| token_input = iv_panel.locator("input[placeholder*='Paris (leading space']") | |
| if not token_input.is_visible(): | |
| page.click("#view-lens summary:has-text('Interventions')") | |
| token_input.fill(" the") | |
| seg_check(iv_panel.locator(".seg-item:has-text('logit') input").first) | |
| page.click("#view-lens button:has-text('Add intervention')") | |
| page.wait_for_selector("#view-lens .iv-table-host tbody tr", timeout=10000) | |
| print(" intervention added") | |
| page.click("#view-lens button:has-text('Generate')") | |
| page.wait_for_selector("#view-lens .token-chip", timeout=60000) | |
| chips = page.locator("#view-lens .token-chip").count() | |
| assert chips > 10, f"expected token chips, got {chips}" | |
| page.wait_for_selector("#view-lens .lens-html-host table", timeout=20000) | |
| print(f" generated, {chips} positions, summary table rendered") | |
| # click two token chips -> selection, update readouts | |
| page.locator("#view-lens .token-chip").nth(2).click() | |
| page.locator("#view-lens .token-chip").nth(3).click() | |
| assert "2 of" in page.text_content("#view-lens .card:has(.token-strip)") | |
| page.click("#view-lens button:has-text('Update readouts')") | |
| page.wait_for_selector( | |
| "#view-lens .status-pane:has-text('2 selected positions')", timeout=30000 | |
| ) | |
| print(" position selection + update readouts") | |
| # switch result views | |
| seg_check(page.locator("#view-lens .seg-item:has-text('Heatmap') input")) | |
| page.wait_for_selector(".lens-html-host p:has-text('Lens readouts')", timeout=15000) | |
| seg_check(page.locator("#view-lens .seg-item:has-text('Readouts') input").first) | |
| page.wait_for_selector(".lens-html-host p:has-text('Readout counts')", timeout=15000) | |
| page.screenshot(path=str(SHOTS / "05-lens.png"), full_page=True) | |
| print("PASS lens view (interventions/generate/selection/views)") | |
| # ---- Analysis view: export from interactive, re-upload | |
| page.click("text=Interactive") | |
| with page.expect_download() as dl: | |
| page.click("#view-interactive button:has-text('Download JSON')") | |
| path = dl.value.path() | |
| print(f" export downloaded: {dl.value.suggested_filename}") | |
| page.click("text=Analysis") | |
| page.set_input_files("#view-analysis input[type=file]", str(path)) | |
| page.wait_for_selector("#view-analysis .status-pane:has-text('Log loaded')", timeout=20000) | |
| assert page.locator("#view-analysis .plot-host .plotly").count() == 2 | |
| assert '"schema_version": 2' in page.inner_text("#view-analysis .json-pane") | |
| page.screenshot(path=str(SHOTS / "06-analysis.png"), full_page=True) | |
| print("PASS analysis view (export round-trip)") | |
| # ---- theme toggle | |
| before = page.get_attribute("html", "data-theme") | |
| page.click(".icon-btn[title='Toggle theme']") | |
| after = page.get_attribute("html", "data-theme") | |
| assert {before, after} == {"dark", "light"}, (before, after) | |
| page.screenshot(path=str(SHOTS / "07-other-theme.png"), full_page=True) | |
| print("PASS theme toggle") | |
| browser.close() | |
| benign = re.compile(r"favicon|manifest|Failed to load resource.*404") | |
| real_errors = [e for e in console_errors if not benign.search(e)] | |
| assert not real_errors, f"console errors: {real_errors}" | |
| print("PASS no console errors") | |
| print("\nALL BROWSER E2E TESTS PASS") | |
| def test_browser_e2e(): | |
| main() | |
| if __name__ == "__main__": | |
| main() | |