Framework Architecture
Overview
This is a Playwright + pytest end-to-end test framework for the forsyt.io mobile web application. It is designed to test the app across multiple device profiles (mobile, tablet, desktop) with full environment configurability, structured logging, artifact capture, and an AI-assisted assertion layer.
Design Principles
Every architectural decision is governed by SOLID:
| Principle | How it is applied |
|---|---|
| S — Single Responsibility | Each class owns exactly one concern: BasePage handles browser interactions, BrowserManager owns the browser lifecycle, Settings owns configuration, DeviceConfig owns emulation data |
| O — Open / Closed | Adding a device profile = one entry in config/devices.py. Adding a page = one new file in pages/. No existing code changes |
| L — Liskov Substitution | All page objects honour the load() / is_loaded() contract. All components honour is_visible(). Substituting one page for another does not break the test layer |
| I — Interface Segregation | BasePage exposes only what every page needs. BaseComponent exposes only what every component needs. Neither has methods that callers can't use |
| D — Dependency Inversion | BrowserManager depends on Settings (abstraction), not on hardcoded values. BaseComponent depends on Settings, not on module-level constants. BasePage.navigate() reads wait_until from DeviceConfig, not from a string literal |
Directory Structure
stryker_web/
│
├── config/ # Configuration layer
│ ├── devices.py # DeviceConfig dataclass + DEVICE_PROFILES registry
│ └── settings.py # Pydantic BaseSettings — single source of truth
│
├── core/ # Framework base classes (do not edit lightly)
│ ├── base_page.py # Abstract base for all page objects
│ ├── base_component.py # Abstract base for all UI components
│ └── browser_manager.py # Playwright browser lifecycle manager
│
├── pages/ # Page Object Models — one file per page
│ ├── login_page.py
│ └── dashboard_page.py
│
├── components/ # Reusable UI component objects
│ └── navbar.py
│
├── tests/
│ └── e2e/ # End-to-end test files
│ ├── test_homepage.py
│ └── test_sample.py
│
├── ai/ # AI-assisted assertion / healing layer
│ ├── assertion_generator.py # Screenshot → assertions via Claude API
│ ├── selector_healer.py # Broken selector repair via Claude API
│ └── visual_analyzer.py # Figma vs live screenshot comparison
│
├── utils/
│ └── logger.py # Loguru-based structured logger
│
├── conftest.py # Root fixtures: settings, browser_manager, page, mobile_page, desktop_page
├── pytest.ini # Markers, logging config, test discovery
├── .env # Local environment (never committed)
├── .env.example # Template — commit this
└── docs/ # You are here
Layer Diagram
┌─────────────────────────────────────────────┐
│ TEST LAYER │
│ tests/e2e/test_*.py │
│ Uses: fixtures, page objects, settings │
└───────────────────┬─────────────────────────┘
│ imports
┌───────────────────▼─────────────────────────┐
│ PAGE OBJECT LAYER │
│ pages/*.py components/*.py │
│ Inherits BasePage Inherits BaseComponent │
└───────────────────┬─────────────────────────┘
│ inherits
┌───────────────────▼─────────────────────────┐
│ CORE LAYER │
│ base_page.py base_component.py │
│ browser_manager.py │
└───────────────────┬─────────────────────────┘
│ reads
┌───────────────────▼─────────────────────────┐
│ CONFIGURATION LAYER │
│ settings.py ──► devices.py │
│ .env / .env.<ENV> │
└─────────────────────────────────────────────┘
Configuration & Environment Switching
All runtime behaviour is driven by environment variables loaded into the Settings object via Pydantic-settings.
How settings are loaded
Priority (highest → lowest):
1. Shell environment variables export BASE_URL=https://x.com
2. .env.<ENV> file .env.staging, .env.prod
3. .env file .env (local dev)
4. Field defaults in Settings class
Key variables
| Variable | Default | Effect |
|---|---|---|
ENV | local | Selects which .env.<ENV> file to load |
BASE_URL | http://localhost:3000 | Root URL under test |
DEVICE | mobile | Device profile (see config/devices.py) |
BROWSER | chromium | chromium / firefox / webkit |
HEADLESS | true | false = visible browser window |
NAVIGATION_WAIT_UNTIL | (profile default) | domcontentloaded / networkidle |
Environment files per environment
.env → local development
.env.dev → dev server
.env.staging → staging environment
.env.prod → production (read-only smoke tests)
Run against staging:
ENV=staging pytest -m smoke
Device Profile System
Device emulation is fully decoupled from tests via the DeviceConfig dataclass in config/devices.py.
Resolution order
CLI flag --device-profile=desktop
↓ overrides
DEVICE= env var DEVICE=desktop
↓ selects
DeviceConfig config/devices.py → DEVICE_PROFILES["desktop"]
↓ applied by
BrowserManager new_context(device_config=...)
↓ affects
BrowserContext viewport, is_mobile, has_touch, device_scale_factor, user_agent
Built-in profiles
| Key | Viewport | Mobile | Touch | DPR |
|---|---|---|---|---|
mobile (default) | 402×874 | yes | yes | 2.0 |
iphone_14_pro | 393×852 | yes | yes | 3.0 |
iphone_se | 375×667 | yes | yes | 2.0 |
pixel_7 | 412×915 | yes | yes | 2.625 |
samsung_s23 | 360×780 | yes | yes | 3.0 |
tablet | 768×1024 | yes | yes | 2.0 |
ipad_pro | 1024×1366 | yes | yes | 2.0 |
desktop | 1280×720 | no | no | 1.0 |
desktop_hd | 1920×1080 | no | no | 1.0 |
desktop_2k | 2560×1440 | no | no | 2.0 |
Fixture Graph
scope: session
settings(request) ← reads --device-profile CLI flag
└── browser_manager(settings)
scope: function (per test)
browser_context(browser_manager, settings, request)
└── page(browser_context, settings) ← active device profile
mobile_page(browser_manager, settings, request) ← forced mobile 402×874
desktop_page(browser_manager, settings, request)← forced desktop 1280×720
authenticated_page(browser_manager, settings, request) ← with auth state + active device
Retry & Resilience
All element interactions in BasePage and BaseComponent are wrapped in _retry():
- Attempts:
settings.retry_attempts(default 3) - Delay:
settings.retry_delay_ms(default 500ms) - On exhaustion: captures a screenshot named
retry_exhausted_<action>.pngthen re-raises - Covers:
PWTimeoutErrorand all other exceptions
Artifact Capture
All artifacts land in settings.artifacts_dir (default: artifacts/):
artifacts/
├── screenshots/ ← taken on failure or manually
├── videos/ ← if RECORD_VIDEO=true
├── traces/ ← if RECORD_TRACE=true (Playwright .zip trace files)
└── logs/
├── pytest.log ← INFO level, all tests
└── debug.log ← DEBUG level, all tests
Trace files can be opened with:
playwright show-trace artifacts/traces/<name>.zip
Adding a New Environment
- Create
.env.uatwithBASE_URL=https://uat.forsyt.io - Run:
ENV=uat pytest -m smoke
No code changes required.
Adding a New Device Profile
Open config/devices.py and add one entry to DEVICE_PROFILES:
"galaxy_fold": DeviceConfig(
name="galaxy_fold",
viewport_width=280,
viewport_height=653,
is_mobile=True,
has_touch=True,
device_scale_factor=3.0,
),
Then: pytest --device-profile=galaxy_fold tests/
No other code changes required.