Skip to main content

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_login as 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.