Back to blog

The Solo Developer's Playwright Strategy: What to Test and What to Skip

You don't have a QA team. You ARE the QA team. Here's how I decide what's worth writing Playwright tests for — and what I deliberately skip.

May 25, 20265 min readtestingplaywrightworkflowsolo-dev

You don't have a QA team. You ARE the QA team.

As a solo developer, you write the code, review the PR, deploy to production, and then pray nothing breaks. Testing feels like a luxury — but shipping broken features to users is even more expensive.

The problem isn't whether to test. It's what to test. Playwright gives you the power to test everything. That doesn't mean you should.

Here's the framework I use to decide.

The 80/20 of E2E Testing

Most bugs that hit users live in a surprisingly small surface area:

  • Auth flows — login, signup, password reset. If users can't get in, nothing else matters.
  • Critical money paths — checkout, payment, subscription changes. Revenue-breaking bugs are existential.
  • Core CRUD — the one thing your app does. Create, edit, delete the main entity.

Everything else is nice-to-have. For a solo dev, "nice-to-have" means "not this sprint."

What to Test First (Tier 1 — Non-Negotiable)

These are your smoke tests. Run them on every PR. If any fail, block the deploy.

// Auth: can a user sign up and log in?
test('user can sign up and reach dashboard', async ({ page }) => {
  await page.goto('/signup');
  await page.fill('[name="email"]', 'test@example.com');
  await page.fill('[name="password"]', 'securePassword123');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

// Core flow: can they do the main thing?
test('user can create a project', async ({ page }) => {
  await page.goto('/dashboard');
  await page.click('text=New Project');
  await page.fill('[name="title"]', 'My Project');
  await page.click('button:text("Create")');
  await expect(page.locator('text=My Project')).toBeVisible();
});

Keep these under 5 tests. Under 2 minutes total runtime. Fast feedback loop or no feedback loop.

What to Test Next (Tier 2 — Weekly)

Once your smoke tests are solid, add coverage for:

  • Form validation — empty fields, invalid emails, max length
  • Permission boundaries — can a regular user access admin routes?
  • API error states — what happens when the backend returns 500?
test('form shows validation error for empty title', async ({ page }) => {
  await page.goto('/dashboard');
  await page.click('text=New Project');
  await page.click('button:text("Create")');
  await expect(page.locator('text=Title is required')).toBeVisible();
});

test('non-admin cannot access admin panel', async ({ page }) => {
  await loginAs(page, 'regular-user');
  await page.goto('/admin');
  await expect(page).toHaveURL('/dashboard');
});

Run these nightly or on main merge. They catch regressions, not blockers.

What to Skip (For Now)

This is the hard part. These tests feel important but burn solo dev time:

UI pixel-perfection — Screenshot tests are brittle. One CSS tweak breaks 20 snapshots. Use visual regression only for your pricing page and nothing else.

Edge cases in secondary features — "What if the user uploads a 50MB PNG to their profile picture?" Sure, but does that ship revenue? No. Ship first, harden later.

Third-party integrations — Don't write E2E tests for Stripe checkout, OAuth redirects, or webhook handlers. Mock them. Trust that Stripe's SDK works.

Mobile responsive layouts — Playwright can emulate devices, but testing every breakpoint is a time sink. Test your primary breakpoint. Lighthouse handles the rest.

// DON'T: screenshot comparison across 5 breakpoints
// DO: just make sure it doesn't break on mobile
test('mobile layout renders without overlap', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto('/dashboard');
  await expect(page.locator('nav')).toBeVisible();
});

The Solo Dev Playwright Config

Here's my lean playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30_000,
  retries: 1,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
  ],
});

One browser. One retry. Traces on failure. That's it. Add Firefox when you have a team.

My Actual Test Folder Structure

e2e/
├── smoke.spec.ts        # Tier 1: auth + core flow
├── critical.spec.ts     # Tier 2: validation + permissions
└── fixtures/
    └── auth.ts          # shared login helper

Three files. No nested test suites. No test matrix. Findability beats organization when it's 2 AM and production is down.

The Mental Model

Ask yourself this before writing any E2E test:

  1. If this breaks, will a user notice within 1 hour? → Write the test.
  2. If this breaks, will I lose money? → Write the test.
  3. If this breaks, will I look unprofessional? → Maybe.
  4. If this breaks, is it just annoying? → Skip it. File an issue. Move on.

Your time is the bottleneck. Every test you write is a feature you didn't ship. Optimize for the tests that prevent the worst outcomes, not the tests that make your coverage report look good.

Coverage Is a Trap

A solo dev with 90% test coverage is a solo dev who shipped half as many features. Coverage measures lines touched, not bugs prevented. Five well-placed smoke tests catch more real bugs than fifty unit tests on utility functions.

Measure differently: how many times did a bug reach production? If the answer is "rarely and never catastrophic," your test strategy works.