This episode covers how to measure unit, integration, and functional test coverage, digging into performance insights with tracing and browser metrics, measuring page load and responsiveness, and introducing snapshot testing to detect visual changes.

How much of the application is really tested? And how fast does the page actually load? This episode 11 addresses two questions that often get skipped while teams are busy adding tests: coverage — making sure all layers are tested — and performance — making sure the application isn't just correct, but also fast.
Both share the same tools: tracing and browser metrics that can be captured directly from Playwright. You'll learn to measure how broad your suite's coverage is, read page timing the way real users experience it, and create visual snapshots that catch unexpected changes in the future.
The test pyramid provides direction: many unit tests at the base, some integration tests in the middle, a few end-to-end tests at the top. To measure coverage comprehensively, you need to monitor those three layers separately:
Playwright doesn't automatically count application line coverage — that's the job of tools at the unit layer. For the end-to-end layer, coverage is measured semantically: an application feature map where each feature maps to a set of tests. A simple example:
login feature → tests/login.spec.ts
checkout feature → tests/checkout.spec.ts
dashboard feature → tests/dashboard.spec.tsWith this map, you can see which features have no tests at all. Healthy end-to-end coverage isn't about line percentages, but about every important business path having its own protection.
For the unit layer, use standard coverage tools like Istanbul or V8 coverage on your unit test runner. Integrate the results into CI so coverage drops are immediately visible. Remember: coverage numbers are just a signal — what matters more is that edge cases and critical flows are genuinely tested.
Modern browsers store timing for every page, which you can read directly from the Playwright context:
import { test, expect } from '@playwright/test';
test('measure page load', async ({ page }) => {
await page.goto('/home');
const timing = await page.evaluate(() => {
const { navigationStart, loadEventEnd, domContentLoadedEventEnd } = performance.timing;
return {
loadMs: loadEventEnd - navigationStart,
domReadyMs: domContentLoadedEventEnd - navigationStart,
};
});
console.log('load duration:', timing.loadMs);
});page.evaluate(() => performance.timing) executes JavaScript inside the page and returns the result to the test. With this, you can measure load duration for every test and flag regressions when the numbers balloon.
For user-facing metrics like Largest Contentful Paint, Playwright makes it easy to capture data via PerformanceObserver:
const lcp = await page.evaluate(async () => {
return await new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
resolve(entries.at(-1)?.startTime ?? 0);
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
});
console.log('LCP (ms):', lcp);The PerformanceObserver captures LCP values in real time. By collecting these metrics across several tests, you build a performance data bank that can be compared across releases.
A trace isn't only for debugging tests — it's also for understanding application behavior:
test.use({ trace: 'on' });
test('investigate checkout performance', async ({ page }) => {
await page.goto('/checkout');
// actions ...
});test.use({ trace: 'on' }) creates a trace for this test. In the trace viewer, the Network and Timing sections show how long each request took — a good starting point for finding slow APIs or large resources.
Beyond loading, measure how quickly the page responds to an interaction — that's the essence of responsiveness:
const start = Date.now();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved')).toBeVisible();
console.log('responsiveness (ms):', Date.now() - start);By measuring the span from click until the resulting element appears, you get an end-to-end picture of how responsive a single action is. Make this a helper so all tests use the same metric.
Metrics without thresholds are just numbers. Set reasonable values and let tests monitor them:
expect(lcp).toBeLessThan(2500);expect(lcp).toBeLessThan(2500) turns a measurement into an assertion. If LCP exceeds 2.5 seconds, the test fails — a performance regression is caught before reaching production.
Snapshot testing captures a baseline image of the page and compares it with the latest version:
await expect(page).toHaveScreenshot('home.png');On the first run, page.toHaveScreenshot('home.png') stores a baseline image; on subsequent runs it compares against the current image. Any pixel difference fails the test, indicating a visual change that hasn't been approved.
Baseline snapshots are stored in a __screenshots__ folder or per your configuration, and must be committed to the repository. When a visual change is intentional, update the baseline with npx playwright test --update-snapshots. The full details of visual regression, including thresholding and layout drift, will be covered in episode 16.
npx playwright test --update-snapshotsEpisode 11 equipped you with two monitoring lenses: coverage that maps every feature to tests at all layers of the pyramid, and performance measured directly from within the browser via the timing API, Core Web Vitals, and tracing. Snapshot testing starts introducing you to automatic visual change detection.
Key takeaways:
page.evaluate captures timing and Core Web Vitals from inside the page.toHaveScreenshot snapshot is the first alarm for visual changes.In the next episode we'll discuss API testing and network interception — mocking API responses with route interception, testing offline mode and network failures, validating backend behavior through UI flows, and integration with API test tools.