From Zero to Playwright: How to Bootstrap an Automation Repo That Scales

From Zero to Playwright: How to Bootstrap an Automation Repo That Scales
“Getting off the ground is just the start; the real goal is reaching orbit.”

You’re starting a brand‑new automation repo that you expect to grow into an enterprise asset. The goal isn’t “get a few tests running.” It’s lay down habits and guardrails so new SDETs can onboard quickly, write clean tests, and avoid the typical graveyard of flaky E2E. This post gives you a practical, copy‑pasteable blueprint using Playwright (Python + pytest), but the structure applies to any stack.


The Philosophy (Before You Write a Line of Code)

  • Small PRs, short branches. Trunk‑based with <300 LOC PRs keeps velocity and code review quality high.
  • Conventional commits. feat:, fix:, test:, docs:, chore:, refactor: make history searchable.
  • Code review rules. Two approvals for framework changes; one for test files. Ban sleeps; justify brittle selectors.
  • PR checklist. Passing CI, tests added/updated, docs updated, traces/screens attached on UI changes.

Day 1: The Minimal but “Right” Repo Skeleton

automation/
  README.md
  CONTRIBUTING.md
  CODEOWNERS
  pyproject.toml
  pytest.ini
  .pre-commit-config.yaml
  envs/
    .env.dev.example
    .env.qa.example
  tests/
    dev/
      smoke/
      regression/
    qa/
    prod/
  shared/
    fixtures/
      conftest.py
    pages/
      auth/
      scheduling/
      visits/
    controls/
      datepicker.py
      table.py
    utils/
      api.py
      time.py
    data/
      factories/
  reports/
  scripts/
    run_local.sh
    seed_dev.py

Why this works:

  • Environment split from the start (tests/dev, tests/qa, tests/prod).
  • Single reuse hub under shared/ for fixtures, page/controls, utils, and test data.
  • Obvious places to put things = faster onboarding and fewer “where does this go?” debates.

Tooling That Enforces Hygiene (Not Opinions)

  • Formatter: Black (or Ruff formatter) — kills style arguments.
  • Linter: Ruff — supersedes flake8 + common plugins.
  • Types: mypy/pyright — gradual typing; start with framework code.
  • Pre‑commit: run ruff, format, mypy, and a tiny smoke subset on push.

pyproject.toml (starter):

[tool.pytest.ini_options]
addopts = "-q -ra --maxfail=1"
testpaths = ["tests"]

[tool.ruff]
select = ["E","F","W","I","UP","B"]
line-length = 100

[tool.black]
line-length = 100

[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true

.pre-commit-config.yaml (starter):

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.5.6
  hooks:
    - id: ruff
    - id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v1.10.0
  hooks:
    - id: mypy
- repo: https://github.com/pytest-dev/pytest
  rev: 8.2.0
  hooks:
    - id: pytest
      stages: [push]
      args: ["-m", "smoke", "-q"]

Config & Secrets (Fast, Safe, Consistent)

  • Load .env per env in conftest.py (e.g., BASE_URL, timeouts, flags).
  • Never check secrets; use CI secrets + .env.*.example for local.
  • Centralize timeouts/retries/tracing defaults in one module.

Selector & Stability Strategy (Non‑Negotiable)

  • Publish data-testid in the product. Use Playwright’s role selectors by default.
  • Ban brittle CSS/XPath unless justified in code review.
  • Remove sleep. Prefer expect auto‑waits or explicit helpers.
  • Favor network‑level waits when you can predict backend activity.

Test Design Principles to Teach Every New Hire

  • Small & modular. One intent per test; deterministic; idempotent.
  • No cross‑test dependencies. Each test creates/owns its data.
  • E2E are precious (<10%). They verify stitching, not minutiae.
  • Fixture‑driven reuse. Auth/session, seeded data, page wiring via fixtures.
  • Parametrize > duplicate. Use factories for variants.
  • Document intent, not click‑by‑click. Use short docstrings + TMS markers.

Test docstring pattern: Purpose / Preconditions / Validates.


Page Objects & Control Objects (The Secret to Scale)

  • Page Objects expose intentions (e.g., filter_upcoming(), submit_hra()), not clicks.
  • Control Objects model reusable widgets (tables, datepickers) to kill copy‑paste across pages.

Minimal page object:

class VisitsPage:
    """Visits page: list, filter, open visit details."""
    def __init__(self, page, base_url: str):
        self.page = page
        self.base_url = base_url

    def open(self):
        self.page.goto(f"{self.base_url}/visits")

    def filter_upcoming(self):
        self.page.get_by_role("tab", name="Upcoming").click()

    def first_row(self):
        return self.page.get_by_role("row").nth(1)  # skip header

A reusable control:

class TableControl:
    """Virtualized table helper with stable row/column access."""
    def __init__(self, page, locator):
        self.page = page
        self.locator = locator

    def row_count(self) -> int:
        return self.page.locator(self.locator).get_by_role("row").count()

    def cell(self, row: int, col: int):
        return self.page.locator(self.locator).get_by_role("row").nth(row).get_by_role("cell").nth(col)

Fixtures That Do the Heavy Lifting

Key fixtures to add early:

  • page/context per test for isolation.
  • auth_context to reuse storage state or login via API.
  • seed_* fixtures that create stable, minimal entities via API and tear them down.
  • feature_flags fixture to flip experiments cleanly and skip unsupported tests.

Example test (docstrings + markers):

import pytest
from playwright.sync_api import expect
from shared.pages.visits.visits_page import VisitsPage

@pytest.mark.smoke
@pytest.mark.tms("SNAP-1234")
def test_upcoming_visits_visible(auth_context, base_url):
    """
    Purpose: Ensure the Upcoming tab lists at least one visit for a seeded patient.
    Preconditions: Patient with a scheduled visit exists (seeded via fixture).
    Validates: JIRA SNAP-1234 acceptance criteria A1, A3 (list visible, row fields).
    """
    page = auth_context
    v = VisitsPage(page, base_url)
    v.open()
    v.filter_upcoming()
    expect(v.first_row()).to_be_visible()

Data Strategy & Environments

  • Prefer factories over hand‑rolled fixtures for entities (e.g., PatientFactory.create() -> calls API).
  • Use synthetic static data only when truly static; otherwise generate with UUID suffixes.
  • Add nightly cleanup for orphaned entities to retain idempotency and speed.

Reporting, Traceability & Flake Management

  • Wire Allure (or equivalent): screenshots, videos, trace attachments on failure.
  • Push TMS IDs via @pytest.mark.tms("ID").
  • Quarantine flow: move known‑flaky tests behind a non‑gating job; still fail that job to keep pressure on fixes.
  • Keep JSON/HTML reports per shard, aggregate for dashboards.

CI That Mirrors Risk

Separate jobs that match how you ship:

  • PR: fast dev-smoke (minutes).
  • Merge to main / Nightly: dev-regression (sharded).
  • Manual/cron: prod-smoke (safe subset against prod).

Guidelines:

  • Shard by file or test count; cache Python venv + browser binaries.
  • Always upload artifacts (trace.zip, video, console logs) on failure.
  • Use markers to select tests:
    pytest -m "smoke and not prod_only" or pytest -m "regression".

Documentation People Actually Read

  • README: quickstart (3 commands), repo tour, selector policy, “how to add a test.”
  • CONTRIBUTING.md: branching, commits, PR checklist, naming, code style.
  • /docs (MkDocs):
    • Page object catalog
    • Controls catalog
    • Data seeding guide
    • CI runbook
    • Flake triage flow
  • ADRs: short “Architecture Decision Records” for key choices (selectors, page model, data strategy). They prevent circular debates six months later.

Guardrails for Growth

  • CODEOWNERS: core SDET group owns framework files; domain owners own tests.
  • Budgets: alert on test files >N seconds; keep slow tests behind @pytest.mark.slow.
  • Deprecation policy: annotate with @deprecated + removal version/date and log in an ADR.

CODEOWNERS example:

/shared/**   @qa-core-sdets
/tests/**    @domain-owners @qa-core-sdets

PR template (short):

## What & Why
<!-- brief intent, risk -->

## Screens/Traces
<!-- attach failing trace or success proof if UI-visible -->

## Checklist
- [ ] Tests added/updated
- [ ] Docs updated (README/ADR if needed)
- [ ] No sleeps; selectors follow policy

From Basic → Complex: A Phased Roadmap

Phase 1: Starter (week 1–2)

  • Single env (dev), smoke only.
  • 1–2 page objects + 1 control; login fixture; .env.dev.
  • Pre‑commit, ruff/black/mypy, pytest markers.

Phase 2: Standard (month 1)

  • Env split (dev/qa/prod), full regression; sharded CI.
  • Data factories + API seeding.
  • Allure wired; TMS IDs; trace/video artifacts.
  • Controls library for table/modal/dropdown.

Phase 3: Advanced (quarter 1)

  • Feature flag fixture + skip/xfail matrix.
  • Quarantine pipeline, flake dashboard, stability SLOs.
  • Contract/API tests to pre‑qualify UI runs.
  • Synthetic data pipeline seeded nightly.

Phase 4: Enterprise (6–12 months)

  • Test taxonomy: unit‑like (controls), component (POs), slice (2–3 pages), a few critical E2E.
  • Coverage dashboards by feature & requirement (Allure + TMS).
  • Compliance: secret scanning, SBOM, audit logs on prod runs.
  • Scaling CI: self‑hosted runners, autoscaling, cost caps, smart test selection.

Naming & Layout Conventions (Make the Repo Readable)

  • Files: test_<feature>_<intent>.py (e.g., test_visits_list_smoke.py).
  • Tests: test_<verb>_<object>[_condition] (e.g., test_create_roster_with_required_fields).
  • Page objects: <Area>Page, <Area>Dialog, <Widget>Control.
  • Helpers: verbs (select_*, fill_*, assert_*).
  • Markers: smoke|regression|slow|prod_only; optional tms("ID"), flaky.

Docstrings & Comments (How Much Is Enough?)

  • Test docstring: 3–5 lines > Purpose, Preconditions, Validates. Link Jira via marker, not prose.
  • Page object docstring: one sentence on scope + gotchas (e.g., virtualized table note).
  • Inline comments only for non‑obvious waits, workarounds, and TODOs with owner + deadline.
  • Type hints on public helpers — they double as living docs.

Week‑1 Onboarding: Your SDET Starter Path

  1. Run smoke locally in one command.
  2. Add one parametric regression test using an existing page object.
  3. Add/extend a control‑level assert to remove duplication.
  4. Wire a TMS ID + short docstring (Purpose/Preconditions/Validates).
  5. Read a trace and fix a flaky wait the “repo way.”

Anti‑Patterns to Ban Early

  • sleep without justification.
  • CSS/XPath for dynamic nodes when data-testid or roles exist.
  • Cross‑test dependencies or shared state.
  • Giant E2E flows that verify 20 things and fail for 1.
  • Click‑by‑click narration in comments that goes stale.
  • Hard‑coded data or credentials in tests.

Copy‑Paste Starters

Smoke command (local):

pytest -m smoke -q

Regression command (local, sharded by file glob):

pytest -m regression tests/dev/regression -q

Playwright install (CI bootstrap):

playwright install --with-deps

Example conftest.py snippet (env + auth):

import os
import pytest
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def base_url():
    env = os.getenv("TEST_ENV", "dev")
    load_dotenv(dotenv_path=f"envs/.env.{env}", override=True)
    return os.environ["BASE_URL"]

@pytest.fixture
def auth_context(base_url):
    with sync_playwright() as pw:
        browser = pw.chromium.launch(headless=True)
        context = browser.new_context(storage_state="storage/auth.json")
        page = context.new_page()
        if not os.path.exists("storage/auth.json"):
            # Fallback login routine (API preferred in real-world)
            page.goto(f"{base_url}/login")
            page.get_by_label("Email").fill(os.environ["USER_EMAIL"])
            page.get_by_label("Password").fill(os.environ["USER_PASSWORD"])
            page.get_by_role("button", name="Sign in").click()
            context.storage_state(path="storage/auth.json")
        yield page
        context.close()
        browser.close()

The Payoff

Start small, but start correct: a clear skeleton, strict hygiene, reusable page/controls, fixture‑driven data, and CI jobs that mirror risk. These habits compound. Six months from now you’ll still be moving fast, without a flake‑ridden, nobody‑understands‑it test suite dragging you down.


👉 Want more posts like this? Subscribe and get the next one straight to your inbox.  Subscribe to the Blog or Follow me on LinkedIn