Skip to main content

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:

PrincipleHow it is applied
S — Single ResponsibilityEach class owns exactly one concern: BasePage handles browser interactions, BrowserManager owns the browser lifecycle, Settings owns configuration, DeviceConfig owns emulation data
O — Open / ClosedAdding a device profile = one entry in config/devices.py. Adding a page = one new file in pages/. No existing code changes
L — Liskov SubstitutionAll 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 SegregationBasePage exposes only what every page needs. BaseComponent exposes only what every component needs. Neither has methods that callers can't use
D — Dependency InversionBrowserManager 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

VariableDefaultEffect
ENVlocalSelects which .env.<ENV> file to load
BASE_URLhttp://localhost:3000Root URL under test
DEVICEmobileDevice profile (see config/devices.py)
BROWSERchromiumchromium / firefox / webkit
HEADLESStruefalse = 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

KeyViewportMobileTouchDPR
mobile (default)402×874yesyes2.0
iphone_14_pro393×852yesyes3.0
iphone_se375×667yesyes2.0
pixel_7412×915yesyes2.625
samsung_s23360×780yesyes3.0
tablet768×1024yesyes2.0
ipad_pro1024×1366yesyes2.0
desktop1280×720nono1.0
desktop_hd1920×1080nono1.0
desktop_2k2560×1440nono2.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>.png then re-raises
  • Covers: PWTimeoutError and 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

  1. Create .env.uat with BASE_URL=https://uat.forsyt.io
  2. 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.