From 94ae750e56cd7d3fb9d6572dfcdb150ba8b709be Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 5 Oct 2026 12:59:39 +0200 Subject: [PATCH] Harden e2e waits against loaded-box flakes; add AGENTS.md --- AGENTS.md | 16 ++++++++++++++++ pyproject.toml | 2 +- tests/conftest.py | 19 ++++++++++++++++++- tests/e2e/admin/index.py | 5 ++--- tests/e2e/admin/posts.py | 5 ++--- tests/e2e/docs.py | 7 +++---- tests/e2e/flyers.py | 4 ++-- tests/e2e/index.py | 12 +++++------- 8 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ad36d21 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# AGENTS.md + +## Commands + +- `make test` - run the full test suite +- `make dev` - run the development task +- `make prod` - start the project + +## Code Map + +- `molodetz` - application source +- `tests` - automated tests + +## Conventions + +- Use ES6 module syntax in JavaScript files. diff --git a/pyproject.toml b/pyproject.toml index fac82c6..b8313a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "molodetz" -version = "1.0.9" +version = "1.0.10" description = "Molodetz, a calm community blog roll." readme = "README.md" requires-python = ">=3.12" diff --git a/tests/conftest.py b/tests/conftest.py index ca7e413..8098aa4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,17 +128,34 @@ def browser(app_server): browser.close() +def _record_console_error(page, message): + if message.type == "error" and "ERR_NETWORK_CHANGED" not in message.text: + page.console_errors.append(message.text) + + @pytest.fixture() def page(browser): context = browser.new_context(viewport={"width": 1280, "height": 900}, locale="nl-NL") page = context.new_page() + page.set_default_timeout(60000) page.console_errors = [] page.on("pageerror", lambda error: page.console_errors.append(str(error))) - page.on("console", lambda message: message.type == "error" and page.console_errors.append(message.text)) + page.on("console", lambda message: _record_console_error(page, message)) yield page context.close() +def goto_ready(page, url): + from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + + page.goto(url, wait_until="domcontentloaded") + try: + page.wait_for_selector("html[data-app=ready]") + except PlaywrightTimeoutError: + page.reload(wait_until="domcontentloaded") + page.wait_for_selector("html[data-app=ready]") + + def pytest_sessionfinish(session, exitstatus): shutil.rmtree(TEST_DATA_DIR, ignore_errors=True) diff --git a/tests/e2e/admin/index.py b/tests/e2e/admin/index.py index f856996..d563a93 100644 --- a/tests/e2e/admin/index.py +++ b/tests/e2e/admin/index.py @@ -1,5 +1,5 @@ # retoor -from tests.conftest import login_admin +from tests.conftest import goto_ready, login_admin ADMIN_PAGES = ["/admin", "/admin/posts", "/admin/joins", "/admin/services", "/admin/services/backup", "/admin/trash", "/admin/settings", "/admin/users", "/admin/audit", "/admin/backups", "/admin/stats", "/notifications", "/profile/api-key", "/docs/api/admin"] @@ -7,8 +7,7 @@ ADMIN_PAGES = ["/admin", "/admin/posts", "/admin/joins", "/admin/services", "/ad def test_admin_pages_have_no_js_errors(page, app_server): login_admin(page, app_server) for path in ADMIN_PAGES: - page.goto(app_server + path, wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + path) page.goto(app_server + "/admin/services/backup", wait_until="domcontentloaded") page.click("[data-tab=logs]") assert page.locator("[data-panel=logs]").is_visible() diff --git a/tests/e2e/admin/posts.py b/tests/e2e/admin/posts.py index 04382a9..5932011 100644 --- a/tests/e2e/admin/posts.py +++ b/tests/e2e/admin/posts.py @@ -1,11 +1,10 @@ # retoor -from tests.conftest import login_admin +from tests.conftest import goto_ready, login_admin def test_login_and_editor_preview(page, app_server): login_admin(page, app_server) - page.goto(app_server + "/admin/posts/new", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + "/admin/posts/new") page.fill("#body", "Een **vette** zin [link](javascript:alert(1)) b") page.wait_for_function("document.querySelector('[data-preview-target] strong')") preview = page.locator("[data-preview-target]").inner_html() diff --git a/tests/e2e/docs.py b/tests/e2e/docs.py index 8a1a11c..df46c55 100644 --- a/tests/e2e/docs.py +++ b/tests/e2e/docs.py @@ -1,17 +1,16 @@ # retoor +from tests.conftest import goto_ready def test_docs_tabs_switch(page, app_server): - page.goto(app_server + "/docs/api/content", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + "/docs/api/content") page.click("[data-endpoint] [data-tab=expected] >> nth=0") assert page.locator("[data-endpoint] [data-panel=expected]").first.is_visible() assert page.console_errors == [] def test_docs_code_blocks_get_highlight_and_copy(page, app_server): - page.goto(app_server + "/docs/api", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + "/docs/api") page.wait_for_selector("dp-code .copy-button") assert page.locator("dp-code pre code.hljs").count() >= 1 assert page.console_errors == [] diff --git a/tests/e2e/flyers.py b/tests/e2e/flyers.py index 94b8c8d..bd7de4f 100644 --- a/tests/e2e/flyers.py +++ b/tests/e2e/flyers.py @@ -1,9 +1,9 @@ # retoor +from tests.conftest import goto_ready def test_lightbox_opens_and_closes(page, app_server): - page.goto(app_server + "/flyers", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + "/flyers") page.locator("[data-lightbox]").first.click() lightbox = page.locator("dp-lightbox") assert lightbox.evaluate("el => el.classList.contains('visible')") diff --git a/tests/e2e/index.py b/tests/e2e/index.py index 20bc5d5..3792be0 100644 --- a/tests/e2e/index.py +++ b/tests/e2e/index.py @@ -1,12 +1,11 @@ # retoor -from tests.conftest import assert_no_horizontal_overflow +from tests.conftest import assert_no_horizontal_overflow, goto_ready PUBLIC_PAGES = ["/", "/roll", "/standard", "/flyers", "/memes", "/people", "/people/retoor", "/join", "/docs", "/docs/welcome", "/docs/api/content", "/docs/component-code", "/terms", "/privacy"] def test_home_renders_without_errors(page, app_server): - page.goto(app_server + "/", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + "/") assert page.locator("h1").first.inner_text().strip().lower() == "molodetz" assert page.locator("nav .nav-links a", has_text="Join").count() == 1 assert page.console_errors == [] @@ -15,8 +14,8 @@ def test_home_renders_without_errors(page, app_server): def test_mobile_nav_toggle(browser, app_server): context = browser.new_context(viewport={"width": 390, "height": 800}) page = context.new_page() - page.goto(app_server + "/", wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + page.set_default_timeout(60000) + goto_ready(page, app_server + "/") assert_no_horizontal_overflow(page) assert not page.locator("#nav-links").is_visible() page.click("[data-nav-toggle]") @@ -26,6 +25,5 @@ def test_mobile_nav_toggle(browser, app_server): def test_public_pages_have_no_js_errors(page, app_server): for path in PUBLIC_PAGES: - page.goto(app_server + path, wait_until="domcontentloaded") - page.wait_for_selector("html[data-app=ready]") + goto_ready(page, app_server + path) assert page.console_errors == []