15 Days of Playwright - Day 15: Building a Scalable Playwright Test Suite
The final day. Pull together everything from the series — locators, fixtures, POM, CI, visual testing, and architecture — into a single coherent strategy for a production-grade Playwright test suite.
Jhonatas Matos

You made it. Fifteen days, fifteen articles, one complete journey from npm init playwright@latest to production-grade test suites. Today we close the loop — not with a new technique, but with a strategy for putting everything together.
What We Covered
Before we look forward, let's map what we built:
| Day | Topic | Key Takeaway |
|-----|-------|-------------|
| 1 | Getting Started | Install Playwright, write the first test |
| 2 | Interactions & Assertions | Semantic locators (getByRole, getByText), expect |
| 3 | Smart Waits | Auto-waiting, waitForURL, never waitForTimeout |
| 4 | API Mocking | page.route(), deterministic test scenarios |
| 5 | File Handling | Uploads, downloads, in-memory buffers |
| 6 | Authentication | storageState, authenticate once, reuse everywhere |
| 7 | Fixtures & Hooks | Custom fixtures, composable test setup |
| 8 | Page Object Model | Encapsulate selectors and actions per page |
| 9 | Configuration & Parallelism | playwright.config.js, workers, multi-browser |
| 10 | Hybrid API/UI Testing | request fixture, API setup + UI validation |
| 11 | Trace Viewer & UI Mode | Post-mortem debugging, interactive development |
| 12 | Visual Testing | toHaveScreenshot, baselines, masking |
| 13 | CI/CD & Sharding | GitHub Actions, retries, --shard, reporters |
| 14 | Test Architecture | Layered fixtures, data factories, environment config |
| 15 | Scalable Suite | Everything together |
The Scalability Checklist
A Playwright suite is scalable when you can answer "yes" to all of these:
- [ ] New team members can write tests without reading implementation code
- [ ] A UI change requires updates in one place (the page object), not dozens of test files
- [ ] Tests run in under 5 minutes in CI (through parallelism and sharding)
- [ ] Flaky tests are identified automatically (retry + flaky tracking)
- [ ] Every PR gets a test report with failure details and trace links
- [ ] Credentials and URLs are in environment variables, not in test code
- [ ] Tests create and destroy their own data — no shared fixtures between tests
- [ ] Visual regressions are caught before deploy
The Complete Architecture
Here is how all the pieces connect in a mature project:
playwright.config.ts ← Global config: browsers, retries, reporters, base URL
.github/workflows/
playwright.yml ← CI pipeline: matrix sharding, artifact upload
tests/
fixtures/
index.ts ← Composed test object (extends base test)
auth.fixture.ts ← Authentication state management
data.fixture.ts ← Test data creation + cleanup
pages/
BasePage.ts ← Shared navigation and utility methods
LoginPage.ts
DashboardPage.ts
RegisterPage.ts
components/
HeaderComponent.ts
ModalComponent.ts
data/
factories.ts ← buildUser(), buildAccount(), etc.
helpers/
api.ts ← Typed API wrapper using request fixture
assertions.ts ← Shared assertion helpers
config.ts ← Environment-aware configuration
specs/
auth/
login.spec.ts ← @smoke @auth
register.spec.ts ← @smoke @auth
banking/
transfer.spec.ts ← @regression @banking
statement.spec.ts ← @regression @banking
visual/
homepage.visual.ts ← @visual
dashboard.visual.ts ← @visual
__snapshots__/ ← Visual baselines committed to Git
The Maintainability Rules
After building many test suites, these rules prevent the most common forms of decay:
Rule 1: Selectors live in one place
Every selector for a page lives in that page's class. If you find yourself writing page.locator('.submit-btn') in a test file, move it to the page object.
Rule 2: Tests own their data
Every test that creates data cleans it up in a fixture teardown, even on failure. No test should depend on another test having run first.
testUser: async ({ request }, use) => {
const user = await createUser(request, buildUser());
await use(user);
await deleteUser(request, user.id); // Runs even when test fails
},
Rule 3: No waitForTimeout anywhere
waitForTimeout is a symptom of a missing explicit wait or a missing assertion. Every occurrence should be replaced with waitForURL, waitForResponse, locator.waitFor(), or an expect assertion that auto-retries.
Rule 4: CI is the source of truth
A test that only passes locally is not passing. Run the full suite in CI on every PR. If it's too slow, shard it — don't skip it.
Rule 5: Flaky tests are bugs
When Playwright marks a test "flaky" (failed once, passed on retry), treat it with the same urgency as a production bug. A flaky test erodes trust in the entire suite.
A Complete Working Example
Here is what a spec file looks like when the architecture is in place:
// tests/specs/banking/transfer.spec.ts
import { test, expect } from '../../fixtures';
import { expectToastMessage } from '../../helpers/assertions';
test.describe('@regression @banking Transfer funds', () => {
test('successful transfer reduces sender balance', async ({
authenticatedPage,
dashboardPage,
testUser,
testRecipient,
}) => {
await dashboardPage.goto();
const initialBalance = await dashboardPage.getBalance();
await dashboardPage.initiateTransfer({
to: testRecipient.email,
amount: 100,
});
await expectToastMessage(dashboardPage.page, 'Transfer completed');
await expect(dashboardPage.balanceAmount).toHaveText(
`$${initialBalance - 100}.00`
);
});
test('transfer to unknown account shows error', async ({
authenticatedPage,
dashboardPage,
}) => {
await dashboardPage.goto();
await dashboardPage.initiateTransfer({
to: 'nobody@nowhere.com',
amount: 50,
});
await expect(dashboardPage.errorMessage).toHaveText('Recipient not found');
});
test('transfer with insufficient funds shows error', async ({
authenticatedPage,
dashboardPage,
testUser,
testRecipient,
}) => {
await dashboardPage.goto();
await dashboardPage.initiateTransfer({
to: testRecipient.email,
amount: 999999,
});
await expect(dashboardPage.errorMessage).toHaveText('Insufficient funds');
});
});
No selectors. No setup. No page.goto. No credentials. The test reads like a specification — and that is the goal.
How to Grow a Suite Incrementally
You do not need all of this from day one. Here is a practical growth path:
Weeks 1–2: Foundation
- Install Playwright, write your first tests
- Use
getByRole,getByLabel,getByText— avoid CSS selectors - Enable traces in CI from the beginning
Weeks 3–4: Page Objects
- Create a page object for each major page you're testing
- Move all locators and actions into the page classes
- Tests import page objects, never raw locators
Month 2: Fixtures and Data
- Create a custom
testobject with your app's fixtures - Build data factories for the entities your tests need
- Every test creates and owns its data
Month 3: CI and Sharding
- Wire up GitHub Actions (or your CI platform)
- Add sharding once the suite takes more than 5 minutes
- Enable retries with trace on first retry
Month 4+: Visual and Advanced
- Add visual tests for critical pages
- Add environment-aware configuration
- Tag tests for smoke / regression / E2E tiers
Resources to Keep Learning
These are the official resources I return to most often:
- Playwright Documentation — comprehensive and well-maintained
- Best Practices — the Playwright team's own guidance
- Page Object Models — canonical POM implementation
- Test Fixtures — the full fixture API
- Trace Viewer — debugging guide
- Sharding — splitting tests across machines
Conclusion
Fifteen days ago we installed Playwright and wrote a test that opened a browser and took a screenshot. Today you have the complete toolkit:
- Reliable locators that survive UI refactors
- Smart waits that eliminate flakiness without fixed timeouts
- Page Object Model that keeps tests maintainable at scale
- API mocking and hybrid testing for comprehensive coverage
- Authentication management that makes login a one-time cost
- Composable fixtures that eliminate setup repetition
- Trace Viewer and UI Mode that make debugging fast
- Visual testing that catches regressions functional tests miss
- CI/CD with sharding that keeps feedback fast regardless of suite size
- Test architecture that makes the suite survive the growth of the application
The tools are ready. The patterns are clear. What comes next is practice — writing real tests for real applications, learning what breaks, and applying what you know to fix it.
Thank you for following along through all 15 days. Keep testing, keep improving, and remember: a test suite is never finished — it grows with the product it protects.
Thank you for reading! See you in the next series! ☕💜