Skip to content

Latest commit

 

History

History
173 lines (135 loc) · 10.9 KB

File metadata and controls

173 lines (135 loc) · 10.9 KB

Architecture

This document explains how the suite is layered, how data flows through a test, which gate enforces which boundary rule, and how the committed .claude/ SDET kit and the tools/ validators mechanically keep the architecture from eroding.

It is the deep companion to the short contract in ../CLAUDE.md and the constraints in CONSTRAINTS.md.


1. The 6 layers

The repository is organised into six layers, pinned in ../tests-config.json. Imports flow in one direction only; a reverse import (e.g. a page object importing a spec) is a build failure, not a code-review nit.

flowchart TD
    specs["specs/<br/>AAA tests, tags, assertions"]
    fixtures["fixtures/<br/>mergeTests composition + DI"]
    pages["pages/<br/>page objects: verb actions"]
    factories["factories/<br/>Fishery + Faker, deterministic"]
    support["support/<br/>purchase flow + boundary oracle (pure)"]
    infra["infra/<br/>zod env loader, helpers"]

    specs --> fixtures
    specs --> support
    fixtures --> pages
    fixtures --> factories
    fixtures --> infra
    pages --> support
    pages --> infra
    factories --> infra
Loading
Layer Path Responsibility Must not
infra/ tests/infra zod-validated env access, cross-cutting helpers import from any layer above
support/ tests/support pure test orchestration: payment-boundary oracle, purchase flow + page interfaces import concrete page objects; contain expect
pages/ tests/pages verb actions over private locators (BasePage + 6 POs) contain expect; import specs
factories/ tests/factories type-safe synthetic data via Fishery + Faker touch the network, Date.now, Math.random, top-level mutables
fixtures/ tests/fixtures compose page objects + factories into injected deps hold business logic
specs/ tests/specs the actual tests — Arrange / Act / Assert inline locators, raw fetch/axios, waitForTimeout

There is no components/, api/, clients/, generated/, or data/ layer. Those were removed during the reorganization; this project is black-box UI E2E with no API contract under our control (see CONSTRAINTS.md §2.1).

The support/ layer depends on page objects only through interfaces (OfferLandingPage, PaymentStep) it declares itself — Dependency Inversion, so purchaseVpn is reusable across the RU and EN sites without knowing their concrete classes.


2. Data flow through one test

A representative happy-path spec (Scenario A — Sign Up) flows like this:

sequenceDiagram
    participant Spec as spec (signup-flow.spec.ts)
    participant Fix as fixture (mergeTests)
    participant Fac as factory (user.factory)
    participant PO as page object (Signup/Payment)
    participant Site as live site (account.freevpnplanet.com)

    Spec->>Fix: request injected pages + data
    Fix->>Fac: build synthetic user (SEED=1234)
    Fac-->>Fix: { email@yopmail.com, password, … }
    Fix->>PO: construct page objects (origin-aware)
    Fix-->>Spec: { homePage, signupPage, paymentMethodsPage, user }
    Spec->>PO: actions (open → login → signup → plan → method)
    PO->>Site: goto / click / programmatic input events
    Site-->>PO: navigation to payment provider boundary
    Spec->>Spec: poll reachedPaymentProvider(url) (STOP — no Pay)
Loading

Key invariants visible above:

  • No baseURL. Three origins are in play, so each page object carries an absolute URL derived from env.BASE_URL_*. goto() is always explicit.
  • Determinism. All synthetic data comes from Faker seeded once in ../tests/factories/_seed.ts (SEED=1234), so the same email/password is produced every run.
  • The payment boundary is a hard stop. Specs poll the pure oracle reachedPaymentProvider(url, origins) in ../tests/support/payment-boundary.ts and never submit card/crypto data (CONSTRAINTS.md §2.3). The oracle has no card-form interaction by construction.

3. Boundary rules → which gate enforces them

The rules are not aspirational. Each is enforced by a script under ../tools/, wired into npm run validate, .husky/pre-commit, and CI. Exit code 0 = pass; non-zero fails the build.

Boundary rule Enforced by Where it runs
Specs don't import locators directly; *Page classes live only under tests/pages/ tools/validate-layout.sh npm run validate, pre-commit, CI
BasePage contains no expect; no raw axios/fetch in specs tools/validate-layout.sh npm run validate, pre-commit, CI
No waitForTimeout, no XPath, no inline page-object construction, no stray console.log tools/lint-ui-spec.ts npm run validate, pre-commit, CI
Factories stay pure: no network, no Date.now/Math.random, no top-level mutable state tools/factory-rules.ts npm run validate, CI
Every fixture calls await use(...); no built-in name collisions; scope sanity tools/fixture-rules.ts npm run validate, CI
Repo health: tooling present, required files exist, .claude/ kit intact, no .env committed tools/verify.sh npm run verify

All scripts read their paths from ../tests-config.json, so a layout change in one place propagates to every gate. See ../tools/README.md for the full table.


4. The .claude/ kit and how it enforces the architecture

The kit is a committed, reusable engineering asset, not prompt history. It has three composable tiers plus a guardrail layer.

flowchart LR
    subgraph commands["Slash commands — one-shot pipelines"]
        c1["/test-new"]
        c2["/test-fix"]
        c3["/flake-hunt"]
    end
    subgraph agents["Subagents — isolated context"]
        a1["test-design-agent"]
        a2["flaky-detective"]
    end
    subgraph skills["Skills — single-responsibility"]
        s1["requirements-to-test-design"]
        s2["gherkin-test-case-author"]
        s3["playwright-test-author-ui/api"]
        s4["test-data-factory-builder"]
        s5["fixture-architect"]
        s6["test-code-reviewer"]
    end

    c1 --> s1 --> s2 --> s3
    s3 --> s4
    s3 --> s5
    s4 --> s6
    s5 --> s6
    c1 -.delegates.-> a1
    c3 -.delegates.-> a2
Loading
  • Skills (.claude/skills/) are the building blocks — each does one thing (design, author, review, analyze).
  • Subagents (.claude/agents/) run a skill in an isolated context so a long design or flake hunt doesn't pollute the main conversation.
  • Slash commands (.claude/commands/) chain skills/agents into repeatable pipelines (/test-new, /test-fix, …).

Mechanical enforcement: hooks + validators

Two independent mechanisms keep the architecture honest regardless of who (human or agent) makes a change:

  1. Claude Code hooks (.claude/settings.json) — guard the agent's actions in real time:
    • PreToolUse Bashguard-bash.sh blocks destructive shell commands.
    • PreToolUse Edit|Writeguard-paths.sh blocks writes to tests/api/generated, snapshots, and .env.
    • PostToolUse Edit|Writetypecheck-touched.sh typechecks touched files immediately (formatting/lint are handled later, by lint-staged).
    • Stop → echoes a smoke-test reminder.
  2. Git-level validators (../tools/) — guard every commit, agent-authored or not, via .husky/pre-commit and CI.

The skills carry portable copies of the same validators (e.g. .claude/skills/playwright-test-author-ui/scripts/lint-ui-spec.ts); the copies in tools/ are this repository's wired-in, active gates. The kit is the template; tools/ is the running instance.

Disabled-by-constraint

For this black-box project, CONSTRAINTS.md §4 disables the OpenAPI-oriented capabilities (api-client-from-openapi, playwright-test-author-api, /spec-sync, contract-drift-watch). They remain committed to demonstrate that the workflow supports contract testing when a repo owns a spec — they are simply switched off here.


5. Implementation notes (the non-obvious "why")

The test code is comment-free; intent is carried by names. The few facts a name cannot convey live here, so the knowledge is not lost.

  • BasePage.toggleHiddenControl(locator) — the offer/gateway radios and the terms checkbox are native inputs that are visually hidden behind custom-styled cards and sit outside the layout viewport. A normal .check() can't reach them, so the helper sets checked and dispatches a bubbling change event to drive the site's JS state.
  • BasePage.fillReactively(locator, value) — Site A's /order/ form (PrimeVue) only enables the Next + payment buttons after input, change, and blur fire; Playwright's native .fill() does not reliably emit change headless. The helper dispatches the full event chain.
  • SignupPage.fillEmail waits for the plan toggle first — that toggle renders only after the Vue form hydrates, so its visibility is a reliable "listeners are attached" marker; without the wait, events fire on an un-hydrated input when arriving from /login/.
  • reachedPaymentProvider(url, origins) — the boundary oracle. Success = navigated off our origins to a provider (or stayed in-house with a gateway= invoice param), and the URL is not a */failed|error|declined page. It never inspects card forms.
  • Known gaps (surfaced as test.fixme / omitted on purpose), see ux-findings.md: the RU site exposes no crypto gateway on /payment/; Site A's email validator is permissive (accepts addresses with no @), so that negative case is documented as a defect rather than asserted as passing.