A production-ready, reusable Playwright monorepo that co-enforces REST API contract correctness and UI behaviour in a single test suite. Built for commercial client projects; uses automationexercise.com as the v1 reference application.
npm ci && npm run -w @framework/e2e-tests test:apiNo credentials or browser required — API tests run against automationexercise.com out of the box. For UI tests and full setup, see Getting started.
- API + UI contract testing — Zod schemas are the single source of truth for API response shapes;
@contracttests validate live responses on every CI run when the reference app is reachable (see Known limitations) - Monorepo architecture — three workspace packages with enforced one-way dependency direction (
apps→packages), each independently testable - CI quality gates — five-job pipeline where API + contract + smoke must pass before regression runs; third-party WAF failures skip gracefully without blocking PRs
- Parallel worker isolation — each Playwright worker authenticates with a dedicated pre-seeded account, eliminating credential contention at any concurrency level
| Package | Role |
|---|---|
packages/api-client |
Schema-agnostic HTTP client — retry, auth strategy, interceptors, Zod validation hook |
packages/contracts |
Hand-authored Zod schemas + inferred TypeScript types for all API response shapes |
packages/ui-core |
Reusable BasePage, BaseComponent, Dialog, DataTable, Form, NavigationMenu |
apps/e2e-tests |
All test suites, fixtures, page objects, services, and configuration |
Dependency direction is strictly one-way: apps → packages. Enforced at lint time via ESLint no-restricted-imports. See docs/architecture.md for full details.
playwright-api-ui-framework/
├── packages/
│ ├── api-client/ # Schema-agnostic HTTP client
│ ├── contracts/ # Zod schemas (workspace-internal)
│ └── ui-core/ # BasePage, BaseComponent, fixtures
└── apps/
└── e2e-tests/ # All suites, page objects, data
| Requirement | Version |
|---|---|
| Node.js | ≥ 20 LTS |
| npm | ≥ 10 |
npm cicp apps/e2e-tests/src/config/environments/.env.local.example \
apps/e2e-tests/src/config/environments/.env.localEdit .env.local and fill in the WORKER_N_EMAIL and WORKER_N_PASSWORD values (see Worker accounts below).
npm run -w @framework/e2e-tests seed:workersThe script creates one test account per Playwright worker and prints:
WORKER_N_EMAIL— copy into.env.localWORKER_N_PASSWORD— add as a GitHub Actions secret namedWORKER_N_PASSWORD
Never commit passwords.
.env.localis git-ignored.
npm run -w @framework/e2e-tests test:apiAll API tests should pass. No browser or credentials are needed for this suite.
npm run -w @framework/e2e-tests test:smoke# API tests — no browser, no credentials
npm run -w @framework/e2e-tests test:api
# Contract drift tests — no browser, no credentials
npm run -w @framework/e2e-tests test:contract
# Smoke suite — fast gate, tagged @smoke
npm run -w @framework/e2e-tests test:smoke
# Full regression suite
npm run -w @framework/e2e-tests test:regression
# Reference-app suite (automationexercise.com specific)
npm run -w @framework/e2e-tests test:reference-app
# Everything
npm run -w @framework/e2e-tests test| Project | Grep / directory | Browser | Credentials | Retries (CI) |
|---|---|---|---|---|
api |
src/tests/api/ |
No | No | 1 |
contract |
@contract tag |
No | No | 1 |
smoke |
@smoke tag |
Yes | Yes | 0 |
regression |
excludes @smoke/@flaky/@contract/@reference-app | Yes | Yes | 2 |
reference-app |
@reference-app tag |
Yes | Yes | 2 |
flaky-quarantine |
@flaky tag |
Yes | Yes | 3 |
Variables are loaded from apps/e2e-tests/src/config/environments/.env.${ENV} then overridden by .env.local.
| Variable | Secret | Required | Default | Description |
|---|---|---|---|---|
ENV |
No | No | staging |
Active environment: staging | production |
API_BASE_URL |
No | Yes | — | REST API base URL |
UI_BASE_URL |
No | Yes | — | Browser base URL |
LOG_LEVEL |
No | No | INFO |
DEBUG | INFO | WARN | ERROR |
MAX_WORKERS |
No | No | 4 |
Playwright parallel workers (must match seeded account count) |
WORKER_N_EMAIL |
No | UI tests | — | Pre-seeded test account email for worker N |
WORKER_N_PASSWORD |
Yes | UI tests | — | Password for worker N — store in GitHub Secrets only |
ALLURE_RESULTS_DIR |
No | No | — | Set to enable Allure reporter (e.g. allure-results) |
Each Playwright worker authenticates using a dedicated pre-seeded account to prevent credential contention during parallel test runs.
- Run
npm run -w @framework/e2e-tests seed:workers. - Copy printed
WORKER_N_EMAILvalues into.env.local. - Add
WORKER_N_PASSWORDvalues as GitHub repository secrets. - Ensure
MAX_WORKERSequals the number of seeded accounts.
| Workflow | Trigger | Required PR check |
|---|---|---|
ci.yml |
Push / PR to main |
Yes |
contract-drift.yml |
Manual (workflow_dispatch) | No |
reference-app.yml |
Manual (workflow_dispatch) | No |
Required PR checks (configure in branch protection rules):
quality-gate— build, typecheck, lint, formatapi-testscontract-testssmoke-tests
The regression job runs only on push to main (after smoke passes). Third-party uptime issues on automationexercise.com never block PR merges — they are isolated to the nightly workflows.
WAF / bot-protection on automationexercise.com. The reference application is occasionally protected by Imunify360 or Cloudflare challenge pages when accessed from GitHub Actions runners. When this happens, the WAF-check fixture detects the block and marks the affected suite as skipped rather than failed — so CI stays green but reports 0 tests executed for that run.
This is a deliberate trade-off: treating a third-party infrastructure block as a test failure would make the CI unreliable for PR gating. The correct long-term fix — running contract tests against a local mock server — is scoped for a future iteration. In the meantime, you can re-run a blocked CI job manually once the WAF clears, or run the full suite locally at any time.
# Build the image
docker build -t pw-framework -f docker/Dockerfile .
# Run the regression suite (credentials via .env.local mounted by compose)
docker compose -f docker/docker-compose.yml run e2e-tests
# Run a specific project
docker compose -f docker/docker-compose.yml run e2e-tests \
npx playwright test --project=api- Create
apps/e2e-tests/src/pages/MyPage.tsextendingBasePage. - Declare locators as
readonly Locatorproperties in the constructor. - Expose business-level methods — assertions belong in tests, not page objects.
- Add a schema file to
packages/contracts/src/schemas/<domain>/. - Export from the domain
index.tsand frompackages/contracts/src/index.ts. - Add a
@contracttest inapps/e2e-tests/src/tests/contract/.
- Create
apps/e2e-tests/src/services/api/MyApiService.ts. - Inject
HttpClientvia constructor. - Pass the relevant schema from
@framework/contractsat the call site — never import schemas insidepackages/api-client.
- Implement the
AuthStrategyinterface frompackages/api-client. - Register it in
AuthManager(e.g., via an env-var switch in the fixture). - No changes to
HttpClientor any test file are needed.
| ADR | Decision |
|---|---|
| ADR-001 | Zod-first hand-authored schemas over OpenAPI codegen |
| ADR-002 | Bearer token via Strategy pattern; per-worker storageState |
| ADR-003 | automationexercise.com as reference application |
| ADR-004 | Package publishing deferred to post-v1 |
Why Zod-first schemas? Hand-authored Zod schemas over OpenAPI codegen keeps contract definitions co-located with tests, gives compile-time type safety across API and UI layers, and avoids the generated-code drift that silently breaks CI.
Why Bearer token Strategy pattern? Auth is injected as a pluggable AuthStrategy so individual tests never call login endpoints directly — this isolates auth complexity, enables per-worker storageState caching, and makes swapping auth mechanisms per environment a one-line fixture change.
Why Playwright fixtures? Fixtures compose the full test context (authenticated client, page objects, pre-seeded data) declaratively — each test states what it needs, not how to set it up. This eliminates setup copy-paste and makes parallel isolation trivially safe.
See docs/architecture.md for the full architectural narrative.
Lazar Endarov — QA / Quality Engineering Lead · LinkedIn