Let’s be real — once your Playwright test suite grows beyond a few dozen tests, things can get chaotic fast.
When I first started using Playwright, I was focused on writing passing tests — that’s the goal, right?
But soon I hit that moment every automation engineer faces:
“Why is this file 500 lines long?”
“Where did I define that login flow again?”
“Why is half of my time spent maintaining tests instead of writing them?”
If you’ve been there, you’re not alone. Today, I’ll share how I organize and scale Playwright tests in a way that’s clean, modular, and honestly — sane.
Your Folder Structure Should Think Like Your App
I’ve made the mistake of lumping all tests into a single tests/ folder. It works for 5 tests. Not 50.
Here’s the structure I now swear by:
pgsql
tests/
├── login/
│ ├── login.spec.ts
│ └── loginHelper.ts
├── dashboard/
│ ├── dashboard.spec.ts
│ └── dashboardHelper.ts
fixtures/
├── test-fixtures.ts
pages/
├── LoginPage.ts
├── DashboardPage.ts
playwright.config.ts
Each folder mirrors part of the app — login, dashboard, reports, etc.
Helper files live beside their tests. Page classes live in pages/.
It’s intuitive, clean, and easier for anyone jumping into the repo.
Use Page Object Model
I used to repeat selectors in every test. Then I found Page Object Model (POM) — and never looked back.
Instead of writing:
ts
await page.fill('#username', 'admin');
await page.fill('#password', 'secret');
await page.click('button[type="submit"]');
I now use:
ts
await loginPage.login('admin', 'secret');
Behind the scenes, that’s just a LoginPage class:
ts
export class LoginPage {
constructor(private page: Page) {}
async login(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
await this.page.click('button[type="submit"]');
}
}
Cleaner. Reusable. Future-proof.
Stop Repeating Setup – Use Fixtures
I can’t tell you how many times I copy-pasted login code into every test — until I discovered Playwright fixtures.
Here’s a basic setup:
ts
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('user', 'pass');
});
Even better, you can define custom fixtures like loggedInPage and just use them in your test.
It’s like handing your future self a debugging gift.
Use Tags to Keep Sanity in CI
Ever wanted to just run the smoke tests? Or only test the admin role?
Use tags:
ts
test('@smoke Login works as expected', async ({ page }) => {
// test code
});
Then run:
bash
npx playwright test --grep @smoke
It’s a small step, but it makes a big difference in large test suites.
Keep Your Tests Independent and Clean
Here’s a hard truth I’ve learned:
If your tests rely on each other, one bug can break your whole suite.
Some things that help me:
Use APIs to create test users or reset data.
Clean up test-created records.
Never let one test depend on another’s leftovers.
Is it extra work up front? Yes.
Does it save you from 2 AM CI failures? Absolutely.
Pro Tips
Use page.pause() liberally during test writing.
Name your tests clearly — your future self will thank you.
Don’t log everything — just the meaningful stuff.
Use parallel projects for testing different roles or environments.
Conclusion
You can have the best test framework in the world…
…but if it’s a mess, it’s going to slow you down.
Test organization isn’t flashy, but it’s one of the most valuable things you can do to scale automation with confidence.
Top comments (0)