Abandon the “debt” of Selenium and automate testing with Playwright: Tactics for survival in the field
4 o'clock in the morning. I get woken up by a notification that my CI has crashed, and I follow the logs to see the inorganic error message "Element not found." The time for being immersed in such Selenium quagmire is over.
Let's be honest. In modern front-end development, choosing Selenium is equivalent to "voluntarily choosing to suffer." Cypress isn't bad, but whenever it behaves strangely due to iframes or separate tabs, our productivity is reduced.
In this article, based on our experience of reducing execution speed from 3 minutes to 45 seconds by migrating to Playwright, we will share our knowledge on building a "non-dead" E2E test environment at the field level.
1. Why Playwright's "Locators" are the best
If you're tired of Selenium's brittle selector specifications, Playwright's Locators API can make a world of difference.
Playwright's Locators are more than just wrappers around CSS selectors. Auto-waiting (wait until an element becomes operable) and Strict mode (throw an exception if multiple elements match) are built in as standard.
# Good example: strict selector specification
# Use get_by_role to enable robust testing based on accessibility
submit_btn = page.get_by_role("button", name="Login")
submit_btn.click()
`
This alone eliminates the classic Selenium-era bug of "clicking and crashing before the element is drawn."
2. Evolve POM (Page Object Model) to “dynamic management”
Simply dividing into classes is not enough. Make it possible to separate the base URL and authentication information by using environment variables.
import os
from playwright.sync_api import Page
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.url = os.getenv("APP_BASE_URL", "https://staging.myapp.com")
self.username = page.get_by_label("username")
self.password = page.get_by_label("password")
def navigate(self):
self.page.goto(f"{self.url}/login")
def login(self, user, pwd):
self.username.fill(user)
self.password.fill(pwd)
self.page.get_by_role("button", name="Send").click()
`
By doing this, switching between local, staging, and CI environments can be completed with a single environment variable. Use GitHub Secrets to manage your secrets, and avoid the folly of hard-coding credentials in your code.
3. The dark side of CI: Asynchronous events and the “infinite loop” trap
Previously, when I was monitoring logs with `page.on('console', ...)`, I manipulated `page` in the callback, eating up memory with circular references and blowing up CI.
Playwright has a powerful asynchronous event-driven model, but if you don't manage your listeners, it can quickly break down.
Lesson: Release the listener as soon as you finish using it.
def test_with_logging(page):
def log_handler(msg):
if "error" in msg.text:
print(f"Detected: {msg.text}")
page.on("console", log_handler)
try:
# test execution
page.goto("/")
finally:
# Design to ensure release
page.remove_listener("console", log_handler)
`
4. GitHub Actions: Don't reinvent the wheel
Creating a Docker container to install your own browser is a waste of time. The professional style is to use official images and use static analysis to play with the "old notation".
Configure `.eslintrc.json` and `flake8` to make deprecated code like `page.waitForNavigation` a build error during CI. This is a line of defense to prevent "dead code" spewed out by AI from entering the production environment.
`
# .github/workflows/e2e.yml
jobs:
test:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.40.0-jammy
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npx playwright test
env:
APP_BASE_URL: ${{ secrets.STAGING_URL }}
`
5. Finally: "types" for continued maintenance
The biggest reason why test code ends up in the trash is ``lack of naming conventions'' and ``confused directory structure.''
Recommended directory structure:
tests/
├── e2e/ # User behavior-based testing
├── fixtures/ # Common processing for API mocks and authentication status
├── pages/ # POM definition
└── conftest.py # Pytest fixture definition
`
The key to test design is complete separation of test data and logic. Use `page.route()` for API mocks to create an environment where tests are not affected by backend instability.
Playwright is not a magic wand. But it's the sharpest knife at the moment for you to focus on "test design". Either you can use the tools to your advantage, or you can be swayed by the tools and spend all your time debugging late at night. Everything depends on the "design ability" of the code you write today.