Playwright is powerful.
That’s also the problem.
It lets teams move fast, and when teams move fast, test quality quietly degrades. You end up with unreadable assertions, inconsistent page objects, slow test runs, and Allure reports nobody trusts.
I stopped trying to “educate” people in code reviews. Instead, I wrote ESLint rules.
These six custom rules now guard our Playwright codebase. They are strict, sometimes annoying, and absolutely worth it.
1. Require descriptive assertion messages
The problem
This failure tells you nothing:
Expected true to be false
At scale, missing assertion messages are a tax on every failure.
What the rule enforces
- Every expect() must include a meaningful message
- Applies to expect.poll() and expect.toPass()
- Empty or whitespace-only messages are forbidden
Bad
await expect(isVisible).toBe(true)
Good
await expect(
isVisible,
'Submit button should be visible after successful validation'
).toBe(true)
Why it matters
Assertion messages are part of your debugging experience.
If a test fails without context, the test is incomplete.
2. Wrap page actions in test.step
The problem
Allure reports that look like this are useless:
Test passed
Or worse, hundreds of meaningless nested steps.
What the rule enforces
- Page-action methods must be wrapped in test.step
- No nested test.step calls
- Utility or getter methods are ignored
- this.commonActions.* is allowed without wrapping
Bad
async submitForm() {
await this.submitButton.click()
}
Good
async submitForm() {
await test.step('Submit the form', async () => {
await this.submitButton.click()
})
}
Why it matters
Reports should tell a story.
If an action is meaningful enough to be a method, it’s meaningful enough to be a step.
3. Enforce a real base page object
The problem
Everyone creates their own “page object style”.
Soon you have:
- Different constructor patterns
- Direct page access everywhere
- Hidden lifecycle bugs
What the rule enforces
- All page objects must extend BasePageObject
- No page fields
- No this.page = page
- Constructor must call super(page)
Bad
class LoginPage {
constructor(page) {
this.page = page
}
}
Good
class LoginPage extends BasePageObject {
constructor(page) {
super(page)
}
}
Why it matters
Consistency beats flexibility.
Base classes exist to remove decisions, not create them.
4. Always use this.page.locator() in constructors
The problem
Using bare page.locator() works… until it doesn’t.
It makes refactoring harder and breaks abstraction.
What the rule enforces
- In constructors, locators must be created via this.page.locator()
- Direct page.locator() usage is forbidden
Bad
this.submitButton = page.locator('#submit')
Good
this.submitButton = this.page.locator('#submit')
Why it matters
Future-proofing.
If your base page ever changes how page is managed, your locators won’t explode.
5. Require a blank line after test()
The problem
Dense tests are harder to scan than failing tests.
What the rule enforces
- A blank line after each test() declaration
Bad
test('login works', async () => {
await login()
})
const user = await getUser()
Good
test('login works', async () => {
await login()
})
const user = await getUser()
Why it matters
This sounds trivial. It isn’t.
Whitespace is structure.
Readable tests are maintained. Unreadable ones get skipped.
6. Run multiple assertions in parallel with Promise.all
The problem
Sequential assertions slow tests for no reason.
This is common and unnecessary:
await expect(a).toBeVisible()
await expect(b).toBeVisible()
await expect(c).toBeVisible()
What the rule enforces
- Multiple expect() calls must run in Promise.all
- Applies to arrays or consecutive awaits
Good
await Promise.all([
expect(a, 'A should be visible').toBeVisible(),
expect(b, 'B should be visible').toBeVisible(),
expect(c, 'C should be visible').toBeVisible(),
])
Why it matters
- Faster tests
- Same reliability
- Clear intent
There’s no upside to doing this sequentially.
What changed after enforcing these rules
- Test failures became actionable
- Allure reports became readable
- Page objects stopped drifting
- Test runtime dropped
- Code reviews got shorter and quieter
Yes, some people complained at first.
Then they stopped writing bad tests.
Who this is for
- Teams with growing Playwright suites
- Projects with multiple contributors
- Anyone tired of “guidelines” nobody follows
If your tests are already perfect, you don’t need this.
If they aren’t, linting is cheaper than discipline.
Final take
Playwright gives you freedom.
ESLint gives you standards.
If you care about test quality at scale, rules beat conventions every time.
If you want these rules open-sourced, documented, or packaged cleanly, do it.
Someone else is fighting the same problems.