Part V: Putting it all Together

Chapter 12: Reference Framework Implementation

This chapter presents a cohesive reference implementation that brings together the architectural concepts discussed throughout the book. The purpose is not to provide a production-ready framework for direct reuse, but to demonstrate how the individual components—page objects, services, flows, and test structure—fit together in a consistent and maintainable design.

The example is intentionally minimal in external complexity and focuses instead on internal structure, separation of concerns, and readability. It assumes Playwright with Python as the execution layer, but the design principles are framework-agnostic.

Project Structure

A well-structured automation suite should make its architectural boundaries immediately visible. The following layout separates concerns into distinct layers: UI interaction, service interaction, orchestration, and tests.

 1 automation/
 2  3 ├── app/
 4 │   ├── pages/
 5 │   │   ├── login_page.py
 6 │   │   ├── dashboard_page.py
 7 │   │   └── base_page.py
 8 │   │
 9 │   ├── services/
10 │   │   ├── user_service.py
11 │   │   └── auth_service.py
12 │   │
13 │   ├── flows/
14 │   │   └── user_flows.py
15 │   │
16 │   └── framework/
17 │       ├── page_manager.py
18 │       └── app.py
19 20 ├── tests/
21 │   ├── test_login.py
22 │   └── test_user_journey.py
23 24 └── conftest.py

Architectural intent

  • pages/ contains UI abstractions only
  • services/ handles API or non-UI system interaction
  • flows/ encapsulates reusable user journeys
  • framework/ provides composition and access layers
  • tests/ remains declarative and intent-focused

This structure enforces a clear separation between interaction, orchestration, and validation.

Core Layer: Page Objects

Page objects encapsulate UI behaviour. They should remain focused on interaction with the page and avoid embedding test logic.

 1 class LoginPage:
 2     def __init__(self, page):
 3         self.page = page
 4         self.username = page.locator("#username")
 5         self.password = page.locator("#password")
 6         self.submit = page.locator("button[type='submit']")
 7 
 8     def open(self):
 9         self.page.goto("https://example.com/login")
10 
11     def login(self, username: str, password: str):
12         self.username.fill(username)
13         self.password.fill(password)
14         self.submit.click()

The same principle applies to all pages: locate, act, and expose meaningful UI operations.

Framework Layer: Page Manager and Application Context

The Page Manager provides controlled access to page objects and ensures consistent construction.

 1 class PageManager:
 2     def __init__(self, page):
 3         self.page = page
 4         self._login = None
 5         self._dashboard = None
 6 
 7     @property
 8     def login(self):
 9         if self._login is None:
10             from app.pages.login_page import LoginPage
11             self._login = LoginPage(self.page)
12         return self._login
13 
14     @property
15     def dashboard(self):
16         if self._dashboard is None:
17             from app.pages.dashboard_page import DashboardPage
18             self._dashboard = DashboardPage(self.page)
19         return self._dashboard

A higher-level application context composes all framework components:

1 class App:
2     def __init__(self, page):
3         self.pages = PageManager(page)

This ensures tests depend on a single, consistent entry point.

Service Layer

Services represent interactions outside the browser, typically API calls or direct system operations used for test setup or validation.

 1 import requests
 2 
 3 class UserService:
 4     def __init__(self, base_url):
 5         self.base_url = base_url
 6 
 7     def create_user(self, username: str, password: str):
 8         response = requests.post(
 9             f"{self.base_url}/users",
10             json={"username": username, "password": password}
11         )
12         response.raise_for_status()
13         return response.json()

Services should remain independent of UI concerns and should not import or reference page objects.

Flow Layer: User Journeys

Flows encapsulate multi-step interactions that represent real user behaviour. They reduce duplication and keep tests declarative.

1 class UserFlows:
2     def __init__(self, app):
3         self.app = app
4 
5     def login(self, username: str, password: str):
6         self.app.pages.login.open()
7         self.app.pages.login.login(username, password)
8         self.app.pages.dashboard.assert_loaded()

Flows should not contain assertions beyond those required to validate transition points. Their role is orchestration, not verification strategy.

Test Layer

Tests consume the framework through the application context. They should remain focused on intent rather than mechanics.

1 def test_user_can_log_in(app):
2     app.pages.login.open()
3     app.pages.login.login("demo_user", "secure_password")
4 
5     app.pages.dashboard.assert_loaded()

A more abstracted version using flows improves readability:

1 def test_user_can_log_in(app, flows):
2     flows.login("demo_user", "secure_password")

Both approaches are valid; the choice depends on the level of abstraction preferred by the team.

End-to-End Scenario Example

A more complete scenario demonstrates how services, flows, and pages can be combined.

Scenario: User creation and login

1 def test_user_creation_and_login(app, services, flows):
2     # Arrange via service layer
3     user = services.user.create_user("new_user", "password123")
4 
5     # Act via UI flow
6     flows.login(user["username"], "password123")
7 
8     # Assert via UI
9     assert app.pages.dashboard.is_visible()

This structure demonstrates a clear separation:

  • Service layer prepares system state
  • Flow layer performs user actions
  • Page layer validates UI state

Design Characteristics

This reference implementation exhibits the following architectural properties:

  • Separation of concerns across pages, services, and flows
  • Centralised access via the application context
  • Declarative test structure with minimal UI noise
  • Composable design, allowing layers to evolve independently

The intent is not to enforce a single pattern, but to provide a stable baseline that can be adapted to different team sizes and application complexities.

The key outcome is not the specific implementation, but the architectural principle: tests should describe behaviour, while the framework handles interaction complexity behind a consistent and well-defined interface.

Chapter 13: Trade-offs and Alternatives

The patterns introduced in earlier chapters provide a structured approach to building and scaling UI test automation with Playwright. However, no single architecture is universally appropriate. The effectiveness of any design depends on team size, system complexity, skill level, and long-term maintenance expectations.

This chapter evaluates the trade-offs inherent in the approaches described throughout this document. It also briefly considers alternative models, including the Screenplay pattern, to provide context for architectural decision-making. The aim is not to prescribe a single correct solution, but to support informed choices based on practical constraints.

Complexity Versus Maintainability

A central tension in test architecture is the balance between initial simplicity and long-term maintainability. Simple approaches, such as direct page object usage within tests, are easy to adopt but tend to degrade as the suite grows. More structured approaches, involving domains, services, and flows, introduce additional layers but improve organisation at scale.

For example, a direct approach might look like this:

1 login_page.open()
2 login_page.login("user@example.com", "password")
3 assert dashboard_page.is_loaded()

This is straightforward and requires minimal abstraction. However, as test coverage increases, repeated logic begins to accumulate across the suite.

By contrast, a layered approach introduces additional structure:

1 flows.auth.login("user@example.com", "password")
2 flows.dashboard.assert_user_is_logged_in()

This reduces duplication and improves readability, but introduces more components to maintain. New contributors must understand the abstraction layers before they can effectively navigate the codebase.

The trade-off is therefore clear: simplicity reduces cognitive overhead initially, while structured architectures improve maintainability over time.

Abstraction Overhead

Each additional layer in a test architecture introduces abstraction overhead. While abstraction improves reuse and clarity when used appropriately, excessive layering can obscure behaviour and make debugging more difficult.

For instance, a failure in a deeply nested flow may require tracing through multiple layers:

  • Test
  • Flow
  • Domain
  • Page object
  • Locator

This can slow down diagnosis, particularly for new team members or in large codebases where familiarity is limited.

A flatter structure reduces this overhead but often results in duplicated logic and less consistent test design. The appropriate level of abstraction depends on the maturity of the project and the stability of the system under test.

A useful guiding principle is that abstraction should reduce, not increase, cognitive load. If a layer does not simplify reasoning about the system, it may be unnecessary.

Flexibility Versus Consistency

Highly structured test architectures promote consistency, which is particularly valuable in larger teams. Patterns such as Page Managers, domain models, and flows ensure that tests are written in a uniform style, making them easier to read and maintain.

However, this consistency can come at the cost of flexibility. Strict architectural rules may discourage experimentation or make it harder to implement edge-case scenarios that do not fit neatly into existing abstractions.

For example, a rigid flow-based approach might obscure low-level interactions that are important for debugging or exploratory testing:

1 flows.checkout.complete_purchase(...)

While concise, this may hide important intermediate states that are useful when diagnosing failures.

In contrast, direct page-level interaction provides full visibility but less structure.

The trade-off here is between enforcing consistency across the suite and allowing flexibility for specialised cases. Most mature suites adopt a hybrid approach, using high-level abstractions for common paths and lower-level interactions for targeted tests.

Maintenance Cost Distribution

Different architectural styles distribute maintenance effort in different ways. A simple Page Object Model places most responsibility on individual tests, which must manage setup, interaction, and assertions directly. This can lead to duplicated effort across the suite.

More layered architectures shift maintenance responsibility into shared components such as flows and services. This centralisation reduces duplication but increases the importance of those shared components being well-designed and stable.

In practical terms:

  • Flat structures distribute complexity across many tests
  • Layered structures concentrate complexity into reusable components

Neither approach eliminates maintenance cost; they simply redistribute it. The appropriate choice depends on whether a team prefers decentralised simplicity or centralised control.

Comparison with the Screenplay Pattern

The Screenplay pattern represents a more formalised alternative to the Page Object Model and its extensions. It introduces actors, abilities, tasks, and interactions as core concepts, aiming to model user behaviour in a highly compositional way.

In Screenplay-based systems:

  • Actors perform tasks using abilities
  • Tasks are composed of smaller interactions
  • Questions are used to retrieve system state

This results in a highly expressive and modular structure, particularly suited to large, complex test suites.

However, it also introduces significant conceptual overhead. The abstraction model is more complex than Page Object–based approaches and may be difficult to justify for smaller teams or simpler systems.

Compared to the patterns described in this document:

Approach Strengths Limitations
Page Objects Simple, widely understood Can become unstructured at scale
Page Manager Improves organisation and discovery Adds an additional access layer
Domain + Flows Highly readable, behaviour-focused Requires disciplined abstraction use
Screenplay Highly scalable and composable High conceptual and implementation cost

The Screenplay pattern is most appropriate where test suites are large, long-lived, and maintained by multiple teams with a strong need for consistency and reuse across complex workflows.

Choosing an Approach

There is no universally optimal architecture for UI test automation. The appropriate design depends on context, including:

  • Team size and experience
  • Stability of the application under test
  • Expected lifespan of the test suite
  • Required speed of development and feedback
  • Complexity of user workflows

Smaller teams or rapidly evolving products may benefit from simpler structures that minimise upfront overhead. Larger organisations with stable systems often benefit from more structured architectures that prioritise long-term maintainability.

In practice, many mature test suites evolve incrementally rather than adopting a single pattern from the outset. They begin with simpler models and introduce additional abstraction layers as complexity demands.

The key consideration is not adherence to a specific pattern, but whether the chosen structure continues to support clarity, reliability, and maintainability as the system evolves.

Chapter 14: Final Considerations

While the preceding chapters have focused on a specific tool and its ecosystem, the underlying concerns - reliability, maintainability, structure, and clarity - are not framework-specific. They remain relevant across different automation tools and even across broader software testing practices.

Let’s consolidate the architectural and design principles discussed so far and reflect on their broader applicability.

Core Principles Revisited

Across the chapters, several recurring principles have emerged. Although they have been expressed in different contexts, they form a consistent set of ideas that underpin sustainable UI test automation.

Clarity of intent

Tests should describe behaviour rather than implementation. Whether expressed through flows, domain models, or direct interactions, the primary goal is to make the purpose of a test immediately understandable.

This principle reduces cognitive load and improves maintainability, particularly as suites grow in size and complexity.

Controlled abstraction

Abstraction is valuable when it simplifies reasoning, but harmful when it obscures behaviour. Layers such as page objects, domain models, services, and flows should each serve a clear and distinct purpose.

When abstractions begin to overlap or hide important details, they become a source of complexity rather than a solution to it.

Isolation and determinism

Reliable tests depend on controlled state. This includes test data management, environment consistency, and avoidance of shared state between tests. Deterministic behaviour ensures that failures are meaningful and reproducible, rather than intermittent or environment-dependent.

Separation of concerns

UI interaction, business logic, and system state management should remain distinct. Page objects, domain models, service layers, and flows each represent different concerns within the system under test. Maintaining clear boundaries between them improves both readability and maintainability.

Tooling Versus Design

A key takeaway from the preceding chapters is that tooling alone does not determine test quality. Playwright provides strong primitives for browser automation, including auto-waiting, locators, and context isolation. However, these capabilities do not automatically produce a maintainable test suite.

Design decisions made at the architectural level have a greater long-term impact than the choice of framework itself. Poorly structured tests can become brittle regardless of the underlying tool, while well-designed abstractions can remain stable even as tools evolve.

Playwright should be understood as an enabler rather than a solution in itself. It provides mechanisms that support good design practices, but does not enforce them.

Portability of Design Principles

Although this document has focused on Playwright and Python, the principles discussed are broadly applicable across different tools and languages.

For example:

  • Selenium-based frameworks benefit from the same separation of concerns between page objects and test logic
  • Cypress-based suites still require careful management of test data and synchronisation
  • API testing frameworks face similar challenges around abstraction, determinism, and maintainability

Even outside UI automation, these principles apply to broader test design and software architecture. Service layering, domain modelling, and controlled abstraction are common patterns in backend systems, distributed systems testing, and integration frameworks.

This portability reinforces the idea that good test architecture is not tool-dependent. It is rooted in general software design principles rather than framework-specific features.

Evolution Over Time

Test suites are not static. They evolve alongside the systems they validate. As applications grow in complexity, test architectures must adapt to accommodate new requirements, workflows, and integration points.

It is common for suites to begin with relatively simple structures and gradually introduce additional layers as needed. Page objects may be extended into domain models, services may be introduced for backend control, and flows may emerge to represent common user journeys.

Architectural complexity tends to accumulate over time, and without periodic review, test suites can become inconsistent or overly complicated.

Regular refactoring and reassessment of abstraction layers help ensure that the test suite continues to serve its primary purpose: providing reliable feedback about system behaviour.

Maintainability as a Long-term Concern

Maintainability is not a static property but an ongoing requirement. A test suite that is easy to understand today may become difficult to maintain as the system evolves or as team composition changes.

Sustainable test design requires continuous attention to:

  • Consistency in abstraction usage
  • Clarity in naming and structure
  • Stability of test data strategies
  • Efficiency of execution and feedback loops

Neglecting these areas often leads to gradual degradation, where tests remain technically functional but become increasingly expensive to maintain.

The most effective test suites are those that are treated as part of the system architecture, rather than as an auxiliary concern.

Closing Perspective

Across all chapters we have persued a consistent theme: reliable UI automation is not achieved through a single pattern or tool, but through the disciplined application of a small set of design principles.

Frameworks such as Playwright provide the mechanisms for interacting with modern web applications, but it is the surrounding architecture that determines whether a test suite remains stable and maintainable over time.

When properly structured, a test suite becomes more than a collection of automated checks. It becomes a readable, maintainable representation of system behaviour—capable of supporting development, validating change, and providing confidence in continuous delivery processes.

Ultimately, the value of any testing approach lies not in its complexity, but in its ability to communicate clearly and remain resilient in the face of change.

Happy testing.