name: ojhunt-e2e description: Playwright e2e browser tests. Load whenever the task involves e2e tests — understanding coverage, planning browser test scenarios, writing or running Playwright tests. See also ojhunt-testing for shared pytest conventions.
E2E Tests (Playwright)
See also the ojhunt-testing skill for shared pytest fixture and assertion conventions.
Setup
- Tests use
test_*.pynaming convention (crawler unit tests use*_test.py) - Marked with
@pytest.mark.playwright— excluded from regular CI - Running: The dev server must be running at
localhost:8080before tests execute. Start it with./doit.sh start(dangerouslyDisableSandbox: true; idempotent). Run visual tests:./doit.sh test-visual(starts server if needed;dangerouslyDisableSandbox: true). Update visual snapshots:./doit.sh update-snapshots(same sandbox requirement). - Visual diff on failure:
conftest.pywritesactual.png,expected.png,diff.pngtotest-results/visual/<name>/when a snapshot mismatch occurs. Or have the user run directly:! uv run pytest -m playwright tests/e2e/ - Install browsers first:
uv run playwright install --with-deps chromium - Always run e2e tests after writing or modifying them — don't mark done until they pass
Test quirks
- localStorage: Persistence tests must clear localStorage before testing:
page.evaluate("localStorage.clear()")thenpage.reload() - Canceled queries: Return to "pending" state (not "canceled"); the query button becomes visible again for retry
- Button locators: Use
.firstwhen multiple buttons exist in a row, e.g.:row.locator("button.remove-btn").first to_have_classmatching: Playwright'sto_have_class(string)does exact matching on the full class attribute, not CSS-token matching. Card elements always carry exactly two classes ("card"+ one status class), so use the full string:to_have_class("card r-ok"),to_have_class("card r-err"), etc. Do not usere.compile(r"r-ok")— it would also match unrelated classes like"foo-ok".- Always intercept crawler API calls: Any test that needs a query result (even as
setup to test something else) must intercept
**/api/crawlers/<name>/<user>withpage.route(...)androute.fulfill(...). Never let e2e tests hit the real crawler network — live calls are flaky and can hit rate limits. Real crawler integration is covered bytest_query.py. The success response shape is:
Register the route before clicking the query button. With a mock the{ "crawler": "<name>", "username": "<user>", "error": False, "data": {"solved": 100, "submissions": 200, "solvedList": ["1A"], "duration": 0.1}, "message": None, }r-oktimeout can be short (10 s); the old 30 s was only needed for real network calls.