A practical guide to making E2E tests “stable” with Playwright: From mere automation to “maintainable assets”

As a specialist editor for technology blogs and gadget reviews, I have carefully reviewed the draft you submitted.

This article has very good content with a "field feel" based on practical experience. While listening to the reader's specific pain of ``Flaciness in E2E testing,'' the book provides logical steps to introduce Playwright not just as a tool, but as an ``operable mechanism.''

When editing, we focused on the following points to improve the content.
1. Improve technical credibility: Proper use of terminology and reinforcement of reasons why the method is recommended.
2. Improve readability: Emphasize headings, organize emphasis, and smoothly connect sentences.
3. Practical Enhancements: Tweaked code example comments and explanations for troubleshooting in the field.

The final version for public use is presented below.


A practical guide to making E2E tests “stable” with Playwright: From mere automation to “maintainable assets”

“Isn’t the E2E test honestly a pain?”

If you are a front-end engineer, you will have this problem at least once. However, as a product grows and features increase, manual testing becomes increasingly limited. In particular, layout breakdowns that only occur in specific browsers and complicated workflows (login → product selection → payment, etc.) that must be manually completed every day are not only a waste of personnel costs, but also cause fatigue for the development team.

What we would like to introduce is E2E testing. However, when you try to implement it, you run into obstacles such as ``tests are flaky'' and ``writing scripts is difficult.''

When I used to use Selenium and Cypress, I had the experience of scattering `sleep` statements here and there to wait for the appearance of elements, and was troubled by the mysterious behavior of ``for some reason failing'' every time I ran it in CI. That's when I came across Playwright.

This time, based on the challenges faced when introducing and operating E2E tests in practice, we will delve into practical points for raising Playwright to a level where it not only works but can be operated stably.


Why Playwright now?

First, let's clarify why so many teams are moving from Cypress to Playwright, or choosing Playwright in the first place. The main reasons can be summarized into the following three points.

1. Ease of multi-browser/multi-device support: You can test major engines such as Chromium, WebKit, and Firefox with one code base. In particular, mobile emulation has high accuracy and can simulate behavior close to the actual device.
2. Strong Auto-waiting: Playwright automatically waits until an element becomes clickable or visible. This is the reason why it has an overwhelming sense of stability compared to Cypress and Selenium.
3. Trace Viewer and advanced debugging features: When a failure occurs in CI, being able to track ``which step, what kind of error occurred, and what was the state of the DOM at that time'' on a timeline can be called a ``God feature'' in the field of operation.

With these characteristics in mind, let's take a look at a comparison table of the main tools.

FeaturesPlaywrightCypressSelenium
Execution speedHigh speed (strong for parallel execution)Medium speed (execution within the browser)Low to medium speed
Browser compatibleChromium, WebKit, FirefoxChrome, Firefox, EdgeWide variety of browsers
Automatic standbyStandard equipment (very powerful)Covered by unique implementationMany cases require manual implementation
Debugging supportTrace Viewer, InspectorTime Travel, GUIBasically logs and screenshots

As you can see from this table, Playwright is currently the most balanced option for rapidly developing and deploying modern web applications.


Practical configuration: From simple scripts to “maintainable test assets”

A common mistake that many teams make is to leave their test code as just a working script. As the project grows, a single CSS class change can cause dozens of tests to break in a chain reaction.

The golden rule to prevent this is to use Page Object Model (POM).

#### Introducing the Page Object Model (POM)
POM is a design pattern that defines elements and actions on a page as "classes." This allows you to separate the UI structure (Locator) and test logic (Action).

For example, when writing a login process, instead of writing `page.click('#login-button')` directly in the test code, pass it through the Page object.

// models/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  private loginButton: Locator;
  private usernameInput: Locator;

  constructor(page: Page) {
    this.page = page;
    // Manage selectors all at once here. Even if the UI structure changes, you only need to fix this part.
    this.loginButton = page.locator('#login-btn');
    this.usernameInput = page.locator('input[name="username"]');
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(username: string) {
    await this.usernameInput.fill(username);
    // Playwright automatically waits until the button becomes clickable
    await this.loginButton.click();
  }
}
`

By defining it like this, even if the ID or class name of the login button changes in the future, there is no need to modify the test body. You only need to update the properties within this class, which can dramatically reduce maintenance costs.

#### Abstraction of common processing using custom fixtures
Playwright has a very powerful mechanism called "Fixtures". This is to automatically perform the necessary setup (remaining logged in, initializing the DB, etc.) before each test.

In particular, sharing authentication information using Storage State is a big time-saver in E2E testing.

// playwright.config.ts (excerpt)
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        // By loading the JSON that saved the authenticated state,
        // You can skip the login process for each test and focus on individual functional tests.
        storageState: 'auth.json',
      },
    },
  ],
});
`

With this setting, there is no need to write the common step "Login → Go to dashboard" in all tests, and you can concentrate on the logic of the function that should be verified.


Pitfalls that are easy to fall into on-site and how to avoid them

When using Playwright in practice, I would like to list three points that I personally experienced that made me think, ``Oh, I'm getting addicted to this.''

#### 1. `waitForTimeout` is strictly prohibited
When a test becomes unstable, there are times when you want to add `await page.waitForTimeout(3000)`. But this is the biggest anti-pattern. Depending on network delays and rendering timing, 3 seconds may not be enough or may result in unnecessary waiting time.

Instead, design it to wait for "state".

  • For simple clicks and inputs, leave it to the actions (`click`, `fill`) provided by Playwright's Locator (these handle the autowait internally).

  • If you need to wait for a specific API response, use `page.waitForResponse()`.


#### 2. Selector selection strategy: User-facing locators
If you use CSS selectors and XPath as is, your tests will easily break due to small changes in the HTML structure. Playwright strongly recommends the use of User-facing locators.

  • ❌ `page.locator('.btn-primary.active')`
  • ✅ `page.getByRole('button', { name: 'Send' })`
By writing tests based on "what the user sees," the tests will pass even if the internal structure changes as long as the behavior does not change. This is a very good habit that also contributes to improving accessibility (a11y).

#### 3. Countermeasures against "Flakiness" in CI environment
It works locally, but crashes in CI (GitHub Actions, etc.). This is often caused by differences in rendering speed due to headless mode or momentary network interruptions.

Practical techniques to solve this:

  • Retries: Set `retries: 2` in `playwright.config.ts` (retries when the game crashes due to a temporary network error, etc.).

  • Trace Viewer: Save trace as artifact when CI fails. This allows you to later check "at what step, what error occurred, and what was the state of the DOM at that time" using videos or snapshots.



Integrating into CI: Practical examples with GitHub Actions

E2E testing only becomes truly valuable when incorporated into CI. Playwright provides official GitHub Actions and has a very smooth integration.

Below is a practical configuration example.

`

name: Playwright E2E Tests
on: [push, pull_request]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: 18
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      # Install required browsers and dependencies during build
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      # Include settings to output a report after execution
      run: npx playwright test
    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: build/playwright-report/
        retention-days: 14
`

By building this workflow, E2E tests will run automatically when a PR (pull request) comes in, and if something fails, you can review the report and quickly make corrections.


Summary: Design the “mechanism” rather than selecting tools

Playwright is a very powerful weapon, but in order to make full use of it, you need a design philosophy of ``how to write tests that are hard to break.''

1. Separate structure and improve maintainability with Page Object Model
2. Use User-facing Locators and choose robust selectors
3. Utilize Fixtures to abstract common processes (such as authentication)
4. Incorporate retries and tracing in CI to reduce debugging costs

By being aware of these things, E2E testing will change from a ``labor-intensive task'' to a ``powerful ally that will help you press the release button with peace of mind.''

Setting up and configuring it may be difficult at first, but once you build a solid foundation, your development speed and confidence in quality will improve dramatically. Why not start with E2E testing of small functions using Playwright?


🤖 Developer blog/public content

The developer's personal blog is also available, so please take a look. muttyan.com blog

Amazon great free trial/benefits summary