Popular searches
//

Why every redesign breaks your Playwright project — and how three layers prevent it

3.7.2026 | 9 minutes reading time

TL;DR: We show how a structural separation of UI selectors and business logic can look like when using Playwright, adapting the proven Robot Pattern into the Layered Robot Pattern. This way, browser automation can proceed without fear of UI changes.


Table of Contents


New design, broken automation

We have used the Robot Pattern over the past two years in two fundamentally different projects: once for regression testing an Android app with Jetpack Compose, and once for server-side control of a web application with Playwright on AWS Lambda. What surprised us was how directly the pattern transfers from one platform to the other. The timing is relevant: AI coding agents are increasingly taking over the creation and maintenance of browser automation. Without clear structural guidelines, they produce arbitrarily structured code that is difficult to control. A consistently followed pattern gives the agent a binding contract. The more disciplined the pattern is maintained, the more reliable the AI assistance becomes. That is exactly what we demonstrate in this article.

A concrete example: a web portal for booking a badminton court at a sports hall is to be automated using Playwright. Login, court selection, time slot selection, booking confirmation — that is four pages with perhaps twenty selectors. What happens to an existing automation when the sports hall redesigns its portal? Alongside new colors, new IDs are assigned, or perhaps an entirely different UI framework is adopted. The Playwright implementation no longer works. Not because the booking process changed — but because the interface evolved.

Why an Android pattern is relevant for Playwright

Jake Wharton introduced the Robot Pattern in 2016 at Kotlin Night in San Francisco [1]. His talk "Testing Robots" addressed a concrete problem: UI tests with Google's Espresso framework that quickly became unmaintainable without structural discipline.

The core idea is simple: separate the what from the how. A Robot encapsulates all UI-specific interactions of a page — selectors, clicks, wait times — behind domain-named methods. The calling code — whether test case or automation workflow — sees only these methods and never a selector.

This sounds like Martin Fowler's Page Object Model [2], and indeed the two are related. The difference lies in ambition: a Page Object primarily encapsulates selectors and exposes them as properties.

A Robot, on the other hand, encapsulates complete actions and ensures that the calling code reads like a domain description.

Playwright explicitly documents the Page Object Model as a recommended pattern [4], but in practice the implementation often stops halfway: selectors are encapsulated but exposed as public Locator properties, rather than hidden behind domain-named methods.

Our answer is the Layered Robot Pattern: a three-layer architecture that combines the selector isolation of the Page Object Model with the action encapsulation of the Robot Pattern, adding an explicit workflow layer.

Getting started quickly with Playwright and AI

Building a Playwright automation is no longer a multi-day project. With playwright codegen, a browser recorder automatically generates type-safe code with robust, semantic selectors — no manual selector hunting required. Playwright MCP goes one step further: an AI agent navigates through the application itself, inspects the DOM, and directly generates a complete PageModel class. A coding agent then takes the generated code and creates a full Robot class following project conventions — including correct inheritance and project structure. What used to require manual refactoring now takes only seconds.

Growing efficiency in day-to-day projects

An effect that only becomes apparent over time: the more Robots exist in the codebase, the more effective the AI assistance becomes. With enough references, the agent recognizes the pattern from context and reliably generates new PageModels, Robots, and even domain workflows. The Layered Robot Pattern acts as a structural contract: it gives the agent clear rules for generated code. Without this pattern, an agent would produce arbitrarily structured Playwright code.

The Layered Robot Pattern in detail

Regardless of the platform, our approach consists of three clearly separated layers. We illustrate them using the badminton court booking example mentioned above.

Layered Robot Pattern - Three-Layer Architecture

The BaseRobot: common foundation for all Robots

All concrete Robots inherit from a common base class. The BaseRobot abstracts Playwright's API — no concrete Robot calls page.fill() or page.click() directly but instead uses the base class methods. This has two advantages: cross-cutting concerns automatically apply to all Robots. And the behavior of all interactions can be changed in exactly one place.

The following is a simplified view:

1export abstract class BaseRobot {
2  protected async click(target: Locator): Promise<void> {
3    await target.click();
4  }
5
6  protected async fill(target: Locator, value: string): Promise<void> {
7    await target.fill(value);
8  }
9
10  protected async waitForSelector(target: Locator): Promise<void> {
11    await target.waitFor({ state: 'visible' });
12  }
13}

The concrete Robot implementation: one class per page

The Robot encapsulates the domain actions of a page. It knows its PageModel; the workflow only sees domain-named methods — never a selector.

1export class BookingRobot extends BaseRobot {
2  private readonly page: BookingPage;
3
4  constructor(page: Page, artifactDir: string) {
5    super(page, artifactDir);
6    this.page = new BookingPage(page);
7  }
8
9  async selectCourt(court: string): Promise<void> {
10    this.logger.info('Selecting court: %s', court);
11    await this.click(this.page.courtSelect);
12    await this.click(this.page.courtOption(court));
13  }
14
15  async bookSlot(date: string, time: string): Promise<void> {
16    this.logger.info('Booking slot: %s %s', date, time);
17    await this.fill(this.page.dateInput, date);
18    await this.click(this.page.timeSlot(time));
19    await this.click(this.page.confirmButton);
20  }
21}

What happens behind the scenes — which dropdown variant the court uses, whether the date is entered via calendar or text field — is an implementation detail that can change at any time without affecting the workflow.

The PageModel: separating selectors from behavior

The PageModel is a separate class per page that exclusively holds selectors — no methods, no logic. A Robot accesses this class; from the outside, it is invisible.

1class BookingPage extends BasePageModel {
2  readonly courtSelect: Locator = this.page.getByLabel('Court');
3  readonly courtOption = (name: string): Locator =>
4    this.page.getByRole('option', { name });
5  readonly dateInput: Locator = this.page.getByLabel('Date');
6  readonly timeSlot = (time: string): Locator =>
7    this.page.getByRole('button', { name: time });
8  readonly confirmButton: Locator = this.page.getByRole('button', { name: 'Book' });
9}

This separation has a concrete advantage: if the target portal changes a form — a label is renamed, a button gets a new role — exactly one file is affected: only the PageModel. The Robot and the workflow remain untouched.

The Workflow: business logic as code

At the top layer sits the workflow. It orchestrates the Robots and reads like a domain description of the booking process:

1export class BadmintonBookingWorkflow {
2  constructor(private readonly config: BadmintonBookingConfig) {}
3
4  async execute(page: Page, artifactDir: string): Promise<void> {
5    const { court, date, time } = this.config;
6
7    const loginRobot = new LoginRobot(page, artifactDir);
8    await loginRobot.login(this.config.email, this.config.password);
9
10    const homeRobot = new HomeRobot(page, artifactDir);
11    await homeRobot.navigateToBooking();
12
13    const bookingRobot = new BookingRobot(page, artifactDir);
14    await bookingRobot.selectCourt(court);
15    await bookingRobot.bookSlot(date, time);
16
17    const confirmRobot = new ConfirmationRobot(page, artifactDir);
18    await confirmRobot.verifyBookingSuccess();
19  }
20}

Anyone reading this code understands the booking process — even without ever having seen the sports hall's portal.

Distinction from BDD

Behavior-Driven Development with Gherkin and Cucumber pursues a similar goal: making domain logic readable. The key difference lies in the overhead: BDD requires feature files, step definitions, and glue code — an additional layer where the mapping between natural language and code relies on Cucumber Expressions or regular expressions. This indirection complicates debugging and makes refactoring fragile: a text change in the feature file breaks steps without the IDE warning.

The Robot Pattern works with pure TypeScript classes. The workflow code is simultaneously documentation and implementation — no separate format that must be kept in sync.

BDD has its place when product owners and testers collaborate on specifications and natural language is the common denominator. For technical automation where developers are the primary audience, the Robot Pattern is more lightweight, type-safe, and maintainable.

Anyone who still prefers BDD can always build it as a layer on top of the Robots — the clean separation of the Robots makes exactly that easy.

Benefits and limitations

The most obvious benefit is maintainability through isolation: UI changes only affect the relevant PageModel (BookingPage) and possibly its associated Robot (BookingRobot). The actual workflow and other Robots remain untouched.

An effect that sets in almost unnoticed is readability. The workflow code reads like a domain description. New team members understand the flow without needing to know the UI details.

At the same time, inheritance from BaseRobot ensures consistency across all Robots. Logging, wait times, error handling — all of this is defined once and reused.

On the other hand, there is the initial complexity. For a flow with three clicks, the Robot Pattern is likely overengineering. The abstraction only pays off when the workflow spans more than two or three pages, when multiple workflows visit the same pages, or when the team consists of more than one person. And even with the pattern, the selector dependency remains — the pattern does not eliminate it, it encapsulates it.

Conclusion

The Layered Robot Pattern is not a new framework or library. It is a structural decision that can be summarized in three rules: one PageModel per page — exclusively selectors. One Robot per page — exclusively domain actions, building on the common base class. Workflows call only Robot methods.

These rules originate from the Android world but work in any UI technology. The investment pays off as soon as a workflow spans more than one page — below that, the overhead is rarely justified. But once complexity grows, the clear separation between what and how pays dividends with every UI change, every new team member, and every debugging session.

What helped us most in day-to-day project work: Playwright records videos of every run on demand — and precisely this has accelerated production debugging the most. A failed run can be traced back to the exact moment in the video, without re-execution, without additional logs.

With today's tools — Playwright MCP and Codegen for selector discovery, AI agents for code generation, headed mode and video recording for debugging and demos — a Robot-based workflow can be built in hours. And the more Robots exist in the project, the more effective the AI assistance becomes: the pattern is the structural contract the agent needs to produce consistent code.

Anyone automating a system via browser — whether for testing or process automation — will find in the Layered Robot Pattern a structure that still works after the next redesign.


References

[1] Jake Wharton, "Testing Robots", Kotlin Night, San Francisco, May 17, 2016 — https://jakewharton.com/testing-robots/

[2] Martin Fowler, "PageObject" — https://martinfowler.com/bliki/PageObject.html

[3] Playwright Documentation, "Codegen" — https://playwright.dev/docs/codegen

[4] Playwright Documentation, "Page Object Model" — https://playwright.dev/docs/pom

[5] Playwright MCP — https://github.com/microsoft/playwright-mcp

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.