Writing tests
Goal: go from "I want to test feature X" to one passing, non-flaky test — using an LLM the right way. Setup is assumed done (see the repo
README.md).
1. Sanity test your environment first
Run a known-good test, headed, and watch it pass before you write anything:
ENV=ci uv run pytest tests/front/test_homepage.py -k test_login --headed
A browser opens, logs in, the test goes green. If it doesn't, fix that before going further — don't debug your environment and your new test at the same time.
2. Verify feature area is working ...
2. Write a one-line test plan
Decide the minimum that proves the feature works. One feature → ideally one test. Writing this down stops the model generating five near-duplicate tests where one would do.
Example: "Create a folder, assert it appears in the workspace tree. That's it."
3. Read the source, then draft the Page Object
Point Claude at the repo that owns the untested feature — octostar-frontend for core platform UI, or the specific app.* repo for an app — and have it derive the real hooks straight from the code: data-testids, roles/labels, component structure. Then draft the Page Object + test from that.
This is faster and cheaper than clicking around a live app to discover selectors — the testids are literally in the source. Requirements for the draft:
- Follow the Page Object pattern
- One page object = one UI surface area
- Give it the test plan from step 2 and point it at
tests/front/test_homepage.py::test_loginas the template.
4. Clean up against the live DOM with playwright-cli
Now open the running app with playwright-cli (see Tools) and verify the drafted selectors actually resolve. Fix what the source didn't reveal: dynamically rendered content, conditional states, elements that only appear after an interaction. This is cleanup/verification — not initial discovery.
❌ Don't point an agent at the live app over MCP and let it click through the whole flow — it burns context and produces brittle, over-specified tests. (See README §5.)
5. Run it headed and watch
ENV=ci uv run pytest tests/front/test_<yours>.py -k <your_test> --headed
--slowmo is on globally so you can follow along. Does it do what your plan said? When it fails → Fixing tests.
Anatomy of a good test
test_login shows the shape in nine lines:
def test_login(self, page: Page) -> None:
homepage = HomePage(page) # interact through a Page Object, never raw locators
user = User(username=..., password=...) # data from services/helpers, not literals
with tracing_group(page, "Step 1: Submit login credentials"):
homepage.login(user) # heavy lifting lives in the Page Object
with tracing_group(page, "Step 2: Verify successful login"):
expect(page.locator(...)).to_be_visible() # assert on a stable, semantic locator
Four habits: Page Objects · fixtures/services for data · tracing_group steps · semantic locators.
Guardrails
Workshop-specific habits — the full convention list is CLAUDE.md §11:
- One feature, one test. If the model gives you five, keep the one that covers the plan.
- Minimum input. Smallest setup that exercises the feature.
page.dispatch_event(...)is an escape hatch. OK only if what you dispatch past isn't what you're testing. Testing the button? Actually click the button.- Headless quirks (clipboard, focus) differ from headed — note them, don't silently special-case.
From CLAUDE.md (pointers, not copies):
- Don't rely on seed data — provision via
helpers/test_helpers.py(rule 13). - No retry/sleep band-aids for flakiness — find the real wait condition.
- UTC only in assertions.