Share page-check helpers in tests/page_check_common.py
MODES, TEST_FILL, FAKE_BRIDGE, the stub loader, the page opener and the no-AI case list now live in one place; sample_data and push move to tests/conftest.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ad73c1fda
commit
344718007e
8 files changed
+125
-189
No files matched your search
+17
-2
@@ -1,6 +1,8 @@
|
|||||||
"""Shared by both page checks: one headless Chromium for the whole run
|
"""Shared by the page checks: one headless Chromium for the whole run
|
||||||
(Playwright's sync API allows only one instance per process)."""
|
(Playwright's sync API allows only one instance per process), and the fixtures
|
||||||
|
that don't depend on which page is under test."""
|
||||||
import pytest
|
import pytest
|
||||||
|
from page_check_common import WIRED, push_to
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
@@ -10,3 +12,16 @@ def browser():
|
|||||||
b = p.chromium.launch()
|
b = p.chromium.launch()
|
||||||
yield b
|
yield b
|
||||||
b.close()
|
b.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_data():
|
||||||
|
"""Use on cases that need the page's own sample data; they skip on a bot-wired page."""
|
||||||
|
if WIRED:
|
||||||
|
pytest.skip("page is wired to a bot, so it has no sample data")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def push(page):
|
||||||
|
"""push(event, data): send one contract envelope to the page, as the bot would."""
|
||||||
|
return lambda event, data: push_to(page, event, data)
|
||||||
@@ -6,42 +6,17 @@ no-AI template is filled with TEST_FILL. Every test runs in light, dark and narr
|
|||||||
The page sees a fake Qt bridge: each slot call lands in window.__calls as [slot, ...args].
|
The page sees a fake Qt bridge: each slot call lands in window.__calls as [slot, ...args].
|
||||||
PAGE_CHECK_WIRED=1: the page is wired to a bot (SAMPLE_DATA off), so sample-data cases skip.
|
PAGE_CHECK_WIRED=1: the page is wired to a bot (SAMPLE_DATA off), so sample-data cases skip.
|
||||||
PAGE_CHECK_REPLAY: a JSON-lines file of envelopes a real bot run produced (bot_runner.py's dry run).
|
PAGE_CHECK_REPLAY: a JSON-lines file of envelopes a real bot run produced (bot_runner.py's dry run).
|
||||||
|
Shared helpers (modes, fill, fake bridge, page opener): tests/page_check_common.py.
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from page_check_common import FAKE_BRIDGE, MODES, TEST_FILL, WIRED, open_page
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
TEMPLATE = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
TEMPLATE = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
||||||
TEST_FILL = {
|
|
||||||
"APP_NAME": "Test Bot",
|
|
||||||
"APP_SUBTITLE": "Page check",
|
|
||||||
"LOGO_SRC": "",
|
|
||||||
"API_BASE": "",
|
|
||||||
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
|
|
||||||
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
|
|
||||||
"CHECKPOINTS_JSON": json.dumps(["Sign in", "Open the list", "Process items", "Finish up"]),
|
|
||||||
}
|
|
||||||
FAKE_BRIDGE = """
|
|
||||||
window.__calls = [];
|
|
||||||
window.qt = {webChannelTransport: {}};
|
|
||||||
window.QWebChannel = function (_transport, ready) {
|
|
||||||
const pyBridge = new Proxy({}, {get: (_o, slot) => (...args) => { window.__calls.push([slot, ...args]); }});
|
|
||||||
ready({objects: {pyBridge}});
|
|
||||||
};
|
|
||||||
"""
|
|
||||||
MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)}
|
|
||||||
WIRED = os.environ.get("PAGE_CHECK_WIRED") == "1"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_data():
|
|
||||||
"""Use on cases that need the built-in sample data; they skip on a bot-wired page."""
|
|
||||||
if WIRED:
|
|
||||||
pytest.skip("page is wired to a bot, so it has no sample data")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -58,28 +33,4 @@ def page_url(tmp_path_factory):
|
|||||||
|
|
||||||
@pytest.fixture(params=list(MODES))
|
@pytest.fixture(params=list(MODES))
|
||||||
def page(request, browser, page_url):
|
def page(request, browser, page_url):
|
||||||
width, height, dark = MODES[request.param]
|
yield from open_page(browser, page_url, request.param, FAKE_BRIDGE)
|
||||||
ctx = browser.new_context(viewport={"width": width, "height": height})
|
|
||||||
ctx.add_init_script(FAKE_BRIDGE)
|
|
||||||
pg = ctx.new_page()
|
|
||||||
pg.set_default_timeout(5000)
|
|
||||||
errors = []
|
|
||||||
pg.on("pageerror", lambda e: errors.append(e))
|
|
||||||
pg.goto(page_url)
|
|
||||||
assert not errors, f"page script errors: {errors}"
|
|
||||||
if pg.get_by_role("form", name="Sign in").is_visible(): # login page section: the host lets us in
|
|
||||||
push_to(pg, "login_result", {"ok": True})
|
|
||||||
if dark:
|
|
||||||
push_to(pg, "apply_dark", {"on": True})
|
|
||||||
yield pg
|
|
||||||
ctx.close()
|
|
||||||
|
|
||||||
|
|
||||||
def push_to(pg, event, data):
|
|
||||||
pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def push(page):
|
|
||||||
"""push(event, data): send one contract envelope to the page, as the bot would."""
|
|
||||||
return lambda event, data: push_to(page, event, data)
|
|
||||||
@@ -6,7 +6,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from conftest import WIRED
|
from page_check_common import WIRED
|
||||||
from playwright.sync_api import expect
|
from playwright.sync_api import expect
|
||||||
from test_contract import colour_of, kpi, open_tab
|
from test_contract import colour_of, kpi, open_tab
|
||||||
|
|
||||||
|
|||||||
@@ -8,32 +8,23 @@ review state directly through the `stub` fixture.
|
|||||||
|
|
||||||
Contract envelopes go in through window.__pyMsg, as in the no-AI check. Each action
|
Contract envelopes go in through window.__pyMsg, as in the no-AI check. Each action
|
||||||
the page POSTs to {API_BASE}/<slot> is recorded in window.__calls as [slot, ...args].
|
the page POSTs to {API_BASE}/<slot> is recorded in window.__calls as [slot, ...args].
|
||||||
Every page test runs in light, dark and narrow mode.
|
Every page test runs in light, dark and narrow mode. Shared helpers: tests/page_check_common.py.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from page_check_common import MODES, TEST_FILL, load_stub, open_page
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
TEMPLATE = ROOT / "rpa-dashboard-ai" / "templates" / "web"
|
TEMPLATE = ROOT / "rpa-dashboard-ai" / "templates" / "web"
|
||||||
TEST_FILL = {
|
TEST_FILL = {**TEST_FILL, "APP_NAME": "Test Extractor", "API_BASE": "/api"}
|
||||||
"APP_NAME": "Test Extractor",
|
|
||||||
"APP_SUBTITLE": "Page check",
|
|
||||||
"LOGO_SRC": "",
|
|
||||||
"API_BASE": "/api",
|
|
||||||
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
|
|
||||||
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
|
|
||||||
}
|
|
||||||
RECORD_ACTIONS = """
|
RECORD_ACTIONS = """
|
||||||
window.__calls = [];
|
window.__calls = [];
|
||||||
const realFetch = window.fetch;
|
const realFetch = window.fetch;
|
||||||
@@ -43,17 +34,9 @@ window.fetch = (url, opts = {}) => {
|
|||||||
return realFetch(url, opts);
|
return realFetch(url, opts);
|
||||||
};
|
};
|
||||||
"""
|
"""
|
||||||
MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)}
|
|
||||||
SKIP_COPY = shutil.ignore_patterns("node_modules", "dist", "__pycache__")
|
SKIP_COPY = shutil.ignore_patterns("node_modules", "dist", "__pycache__")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_data():
|
|
||||||
"""Shared no-AI cases that need the page's own sample data skip on a bot-wired page."""
|
|
||||||
if os.environ.get("PAGE_CHECK_WIRED") == "1":
|
|
||||||
pytest.skip("page is wired to a bot, so it has no sample data")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def site():
|
def site():
|
||||||
"""The dashboard folder under test: server_stub.py, index.html and the sources."""
|
"""The dashboard folder under test: server_stub.py, index.html and the sources."""
|
||||||
@@ -81,11 +64,7 @@ def built(site):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def stub_module(site):
|
def stub_module(site):
|
||||||
sys.path.insert(0, str(site)) # the stub imports bands.py from its own folder
|
return load_stub(site)
|
||||||
spec = importlib.util.spec_from_file_location("server_stub", site / "server_stub.py")
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
return mod
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -110,26 +89,4 @@ def page_url(built, server):
|
|||||||
|
|
||||||
@pytest.fixture(params=list(MODES))
|
@pytest.fixture(params=list(MODES))
|
||||||
def page(request, browser, page_url, stub):
|
def page(request, browser, page_url, stub):
|
||||||
width, height, dark = MODES[request.param]
|
yield from open_page(browser, page_url, request.param, RECORD_ACTIONS)
|
||||||
ctx = browser.new_context(viewport={"width": width, "height": height})
|
|
||||||
ctx.add_init_script(RECORD_ACTIONS)
|
|
||||||
pg = ctx.new_page()
|
|
||||||
pg.set_default_timeout(5000)
|
|
||||||
errors = []
|
|
||||||
pg.on("pageerror", lambda e: errors.append(e))
|
|
||||||
pg.goto(page_url)
|
|
||||||
assert not errors, f"page script errors: {errors}"
|
|
||||||
if dark:
|
|
||||||
push_to(pg, "apply_dark", {"on": True})
|
|
||||||
yield pg
|
|
||||||
ctx.close()
|
|
||||||
|
|
||||||
|
|
||||||
def push_to(pg, event, data):
|
|
||||||
pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def push(page):
|
|
||||||
"""push(event, data): send one contract envelope to the page, as the bot would."""
|
|
||||||
return lambda event, data: push_to(page, event, data)
|
|
||||||
@@ -2,18 +2,6 @@
|
|||||||
|
|
||||||
They use this folder's `page` and `push` fixtures, so they check the React page.
|
They use this folder's `page` and `push` fixtures, so they check the React page.
|
||||||
"""
|
"""
|
||||||
import sys
|
from page_check_common import no_ai_cases
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "page_check"))
|
globals().update(no_ai_cases())
|
||||||
|
|
||||||
from test_contract import ( # noqa: E402,F401
|
|
||||||
test_dark_toggle_flips_theme,
|
|
||||||
test_donut_and_legend_match_batch_states,
|
|
||||||
test_kpi_counts_match_batch_states,
|
|
||||||
test_log_lines_show_their_type,
|
|
||||||
test_no_bot_page_runs_on_sample_data,
|
|
||||||
test_plain_browser_first_open_shows_sample_data,
|
|
||||||
test_report_search_filters_rows_and_shows_bucket_pill,
|
|
||||||
test_step_list_follows_step_label,
|
|
||||||
)
|
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
AI sections (rpa-dashboard-ai/templates/desktop/ai_sections.html) pasted in before </body>.
|
AI sections (rpa-dashboard-ai/templates/desktop/ai_sections.html) pasted in before </body>.
|
||||||
|
|
||||||
Page under test: set PAGE_CHECK_AI_DESKTOP_DIR to a generated dashboard folder (the one with
|
Page under test: set PAGE_CHECK_AI_DESKTOP_DIR to a generated dashboard folder (the one with
|
||||||
dashboard.html and server_stub.py). Without it, the no-AI template is filled with the no-AI
|
dashboard.html and server_stub.py). Without it, the no-AI template is filled with the shared
|
||||||
check's TEST_FILL with no optional sections (sections.py) and the AI sections are pasted in,
|
TEST_FILL (tests/page_check_common.py) with no optional sections (sections.py) and the AI
|
||||||
the way the skill does it.
|
sections are pasted in, the way the skill does it.
|
||||||
|
|
||||||
The page sees a fake Qt bridge, as in the no-AI check: each slot call lands in window.__calls.
|
The page sees the shared fake Qt bridge: each slot call lands in window.__calls.
|
||||||
The AI slot, ai_request(id, method, path, body), goes to the review backend's dispatch() in
|
The AI slot, ai_request(id, method, path, body), goes to the review backend's dispatch() in
|
||||||
this process, and the reply comes back as an `ai_response` envelope, exactly what
|
this process, and the reply comes back as an `ai_response` envelope, exactly what
|
||||||
ai_bridge.py's slot does. So the approve gate under test is the Python one. Tests set run and
|
ai_bridge.py's slot does. So the approve gate under test is the Python one. Tests set run and
|
||||||
@@ -16,41 +16,14 @@ import importlib.util
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from page_check_common import FAKE_BRIDGE, MODES, TEST_FILL, WIRED, load_stub, open_page
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
NO_AI = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
NO_AI = ROOT / "rpa-dashboard-no-ai" / "templates" / "dashboard.html"
|
||||||
AI = ROOT / "rpa-dashboard-ai" / "templates"
|
AI = ROOT / "rpa-dashboard-ai" / "templates"
|
||||||
TEST_FILL = {
|
|
||||||
"APP_NAME": "Test Extractor",
|
|
||||||
"APP_SUBTITLE": "Page check",
|
|
||||||
"LOGO_SRC": "",
|
|
||||||
"API_BASE": "",
|
|
||||||
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
|
|
||||||
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
|
|
||||||
}
|
|
||||||
FAKE_BRIDGE = """
|
|
||||||
window.__calls = [];
|
|
||||||
window.qt = {webChannelTransport: {}};
|
|
||||||
window.QWebChannel = function (_transport, ready) {
|
|
||||||
const pyBridge = new Proxy({}, {get: (_o, slot) => (...args) => {
|
|
||||||
window.__calls.push([slot, ...args]);
|
|
||||||
if (slot === 'ai_request') window.__aiRequest(...args).then(window.__pyMsg);
|
|
||||||
}});
|
|
||||||
ready({objects: {pyBridge}});
|
|
||||||
};
|
|
||||||
"""
|
|
||||||
MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)}
|
|
||||||
WIRED = os.environ.get("PAGE_CHECK_WIRED") == "1"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_data():
|
|
||||||
if WIRED:
|
|
||||||
pytest.skip("page is wired to a bot, so it has no sample data")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -80,11 +53,7 @@ def page_url(site):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def stub_module(site):
|
def stub_module(site):
|
||||||
sys.path.insert(0, str(site)) # the stub imports bands.py from its own folder
|
return load_stub(site, "desktop_server_stub")
|
||||||
spec = importlib.util.spec_from_file_location("desktop_server_stub", site / "server_stub.py")
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
return mod
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -102,27 +71,5 @@ def ai_request(stub, req_id, method, path, body):
|
|||||||
|
|
||||||
@pytest.fixture(params=list(MODES))
|
@pytest.fixture(params=list(MODES))
|
||||||
def page(request, browser, page_url, stub):
|
def page(request, browser, page_url, stub):
|
||||||
width, height, dark = MODES[request.param]
|
expose = ("__aiRequest", lambda i, method, path, body: ai_request(stub, i, method, path, body))
|
||||||
ctx = browser.new_context(viewport={"width": width, "height": height})
|
yield from open_page(browser, page_url, request.param, FAKE_BRIDGE, expose)
|
||||||
ctx.expose_function("__aiRequest", lambda i, method, path, body: ai_request(stub, i, method, path, body))
|
|
||||||
ctx.add_init_script(FAKE_BRIDGE)
|
|
||||||
pg = ctx.new_page()
|
|
||||||
pg.set_default_timeout(5000)
|
|
||||||
errors = []
|
|
||||||
pg.on("pageerror", lambda e: errors.append(e))
|
|
||||||
pg.goto(page_url)
|
|
||||||
assert not errors, f"page script errors: {errors}"
|
|
||||||
if dark:
|
|
||||||
push_to(pg, "apply_dark", {"on": True})
|
|
||||||
yield pg
|
|
||||||
assert not errors, f"page script errors: {errors}"
|
|
||||||
ctx.close()
|
|
||||||
|
|
||||||
|
|
||||||
def push_to(pg, event, data):
|
|
||||||
pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def push(page):
|
|
||||||
return lambda event, data: push_to(page, event, data)
|
|
||||||
@@ -25,17 +25,9 @@ from test_ai_cost import ( # noqa: E402,F401
|
|||||||
test_spend_tiles_sum_the_priced_runs,
|
test_spend_tiles_sum_the_priced_runs,
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "page_check"))
|
from page_check_common import no_ai_cases # noqa: E402
|
||||||
from test_contract import ( # noqa: E402,F401
|
|
||||||
test_dark_toggle_flips_theme,
|
globals().update(no_ai_cases())
|
||||||
test_donut_and_legend_match_batch_states,
|
|
||||||
test_kpi_counts_match_batch_states,
|
|
||||||
test_log_lines_show_their_type,
|
|
||||||
test_no_bot_page_runs_on_sample_data,
|
|
||||||
test_plain_browser_first_open_shows_sample_data,
|
|
||||||
test_report_search_filters_rows_and_shows_bucket_pill,
|
|
||||||
test_step_list_follows_step_label,
|
|
||||||
)
|
|
||||||
|
|
||||||
TAMPER = """async (path) => {
|
TAMPER = """async (path) => {
|
||||||
const reply = new Promise(done => { const base = window.__pyMsg;
|
const reply = new Promise(done => { const base = window.__pyMsg;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""Shared by the three page checks (page_check, page_check_ai, page_check_ai_desktop):
|
||||||
|
the modes, the test fill, the fake Qt bridge, the stub loader and the page opener."""
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MODES = {"light": (1280, 820, False), "dark": (1280, 820, True), "narrow": (800, 900, False)}
|
||||||
|
WIRED = os.environ.get("PAGE_CHECK_WIRED") == "1"
|
||||||
|
TEST_FILL = {
|
||||||
|
"APP_NAME": "Test Bot",
|
||||||
|
"APP_SUBTITLE": "Page check",
|
||||||
|
"LOGO_SRC": "",
|
||||||
|
"API_BASE": "",
|
||||||
|
"STEPS_JSON": json.dumps(["Initializing", "Login", "Process items", "Completed"]),
|
||||||
|
"REPORT_COLUMNS_JSON": json.dumps([{"key": "item", "label": "Item"}, {"key": "status", "label": "Status"}]),
|
||||||
|
"CHECKPOINTS_JSON": json.dumps(["Sign in", "Open the list", "Process items", "Finish up"]),
|
||||||
|
}
|
||||||
|
# Each slot call lands in window.__calls as [slot, ...args]. The AI desktop check exposes
|
||||||
|
# window.__aiRequest, which answers ai_request with an ai_response envelope, as ai_bridge.py does.
|
||||||
|
FAKE_BRIDGE = """
|
||||||
|
window.__calls = [];
|
||||||
|
window.qt = {webChannelTransport: {}};
|
||||||
|
window.QWebChannel = function (_transport, ready) {
|
||||||
|
const pyBridge = new Proxy({}, {get: (_o, slot) => (...args) => {
|
||||||
|
window.__calls.push([slot, ...args]);
|
||||||
|
if (slot === 'ai_request' && window.__aiRequest) window.__aiRequest(...args).then(window.__pyMsg);
|
||||||
|
}});
|
||||||
|
ready({objects: {pyBridge}});
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
# The no-AI contract cases every AI page must also pass, unchanged.
|
||||||
|
NO_AI_CASES = [
|
||||||
|
"test_dark_toggle_flips_theme",
|
||||||
|
"test_donut_and_legend_match_batch_states",
|
||||||
|
"test_kpi_counts_match_batch_states",
|
||||||
|
"test_log_lines_show_their_type",
|
||||||
|
"test_no_bot_page_runs_on_sample_data",
|
||||||
|
"test_plain_browser_first_open_shows_sample_data",
|
||||||
|
"test_report_search_filters_rows_and_shows_bucket_pill",
|
||||||
|
"test_step_list_follows_step_label",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def no_ai_cases():
|
||||||
|
"""{name: test} for NO_AI_CASES; a test module runs them with globals().update(no_ai_cases())."""
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "page_check"))
|
||||||
|
import test_contract
|
||||||
|
return {name: getattr(test_contract, name) for name in NO_AI_CASES}
|
||||||
|
|
||||||
|
|
||||||
|
def load_stub(site, name="server_stub"):
|
||||||
|
"""Import site/server_stub.py. It imports bands.py and pricing.py from its own folder."""
|
||||||
|
sys.path.insert(0, str(site))
|
||||||
|
spec = importlib.util.spec_from_file_location(name, Path(site) / "server_stub.py")
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def push_to(pg, event, data):
|
||||||
|
pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data])
|
||||||
|
|
||||||
|
|
||||||
|
def open_page(browser, url, mode, init_script, expose=None):
|
||||||
|
"""Generator for a `page` fixture: opens url in one mode, signs in through a login
|
||||||
|
section if the page has one, switches to dark, and fails on any page script error."""
|
||||||
|
width, height, dark = MODES[mode]
|
||||||
|
ctx = browser.new_context(viewport={"width": width, "height": height})
|
||||||
|
if expose:
|
||||||
|
ctx.expose_function(*expose)
|
||||||
|
ctx.add_init_script(init_script)
|
||||||
|
pg = ctx.new_page()
|
||||||
|
pg.set_default_timeout(5000)
|
||||||
|
errors = []
|
||||||
|
pg.on("pageerror", lambda e: errors.append(e))
|
||||||
|
pg.goto(url)
|
||||||
|
assert not errors, f"page script errors: {errors}"
|
||||||
|
if pg.get_by_role("form", name="Sign in").is_visible(): # login page section: the host lets us in
|
||||||
|
push_to(pg, "login_result", {"ok": True})
|
||||||
|
if dark:
|
||||||
|
push_to(pg, "apply_dark", {"on": True})
|
||||||
|
yield pg
|
||||||
|
assert not errors, f"page script errors: {errors}"
|
||||||
|
ctx.close()
|
||||||
Reference in new issue
Block a user