Playwright templates for Browser Checks

Updated April 23, 2026

Twelve ready-to-paste Playwright scripts, each chosen to cover a distinct slice of real-world browser monitoring. Copy any of them into your Browser Check editor, adjust the URLs and selectors for your own application, and save.

📘Runtime compatibility

These templates target the 2026.05 runtime (Playwright 1.59.1, Node 24) and pick up Web Vitals on every navigation. Runtime 2023.04 works for most of them, except the ones that rely on test.step reporting, toHaveScreenshot, or devices presets. See Browser Checks for runtime details.

How to use a template

Step 01
Create a browser check
In app.hyperping.io click New monitor, pick Browser as the protocol, and give it a name.
Step 02
Paste a template
Replace the default script with any template below. Update the target URL, selectors, and assertions to match your app.
Step 03
Add secrets and run
Put tokens and credentials in Environment variables, reference them as process.env.NAME, then click Run to test before saving.

Choosing the right angle

Each template teaches a different Playwright primitive. Pick the one closest to what you actually need to watch, then mix techniques across scripts as your coverage grows.

Smoke
01 Homepage health check — the one-minute starter.
Auth
02 Login flow, 11 Storage state — secrets vs. session reuse.
Data
03 Signup, 06 API probe — dynamic inputs and JSON assertions.
Journey
04 Search, 05 Checkout — single action and multi-step with test.step.

Templates

TEMPLATE 01Homepage health check

The baseline. Navigate to a URL, confirm the page rendered, and exit. Use this as a cheap heartbeat when you only need to know that the origin is reachable and not serving a blank or error shell.

01-homepage.spec.tsTS
import { test, expect } from '@playwright/test';

test('homepage loads and renders', async ({ page }) => {
  await page.goto('https://hyperping.io');

  await expect(page).toHaveTitle(/Hyperping/);
  await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});

TEMPLATE 02Login flow

Form authentication with credentials pulled from environment variables. The check fails the moment login breaks, whether the cause is a bad deploy, expired session cookies, or a misconfigured OAuth provider.

02-login.spec.tsTS
import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('https://app.example.com/login');

  await page.getByLabel('Email').fill(process.env.EMAIL);
  await page.getByLabel('Password').fill(process.env.PASSWORD);
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL(/\/dashboard/);
  await expect(page.getByTestId('user-menu')).toBeVisible();
});

TEMPLATE 03Signup with dynamic data

Build a unique email per run from a timestamp so the signup endpoint never collides with a previous run. Useful for verifying the full account-creation pipeline, including downstream welcome emails.

03-signup.spec.tsTS
import { test, expect } from '@playwright/test';

test('new user can sign up', async ({ page }) => {
  const stamp = Date.now();
  const email = `qa+${stamp}@example.com`;

  await page.goto('https://app.example.com/signup');
  await page.getByLabel('Work email').fill(email);
  await page.getByLabel('Password').fill(`Test-${stamp}!`);
  await page.getByRole('button', { name: 'Create account' }).click();

  await expect(page.getByText(/check your inbox/i)).toBeVisible();
});

TEMPLATE 04Search and result verification

Keyboard input, <code>Enter</code> submission, and a count assertion on the result grid. Covers the common case where the failure mode is an empty result page rather than an HTTP error.

04-search.spec.tsTS
import { test, expect } from '@playwright/test';

test('search returns matching results', async ({ page }) => {
  await page.goto('https://example-shop.com');

  const box = page.getByPlaceholder('Search products');
  await box.fill('running shoes');
  await box.press('Enter');

  await expect(page).toHaveURL(/q=running\+shoes/);
  const results = page.getByTestId('product-card');
  await expect(results.first()).toBeVisible();
  expect(await results.count()).toBeGreaterThan(0);
});

TEMPLATE 05Multi-step checkout

Wraps each stage in test.step so the Browser Check run view shows each step as it passes or fails. The step tree gives on-call engineers the exact breakpoint instead of a wall of logs.

05-checkout.spec.tsTS
import { test, expect } from '@playwright/test';

test('guest can complete a checkout', async ({ page }) => {
  await test.step('open product', async () => {
    await page.goto('https://example-shop.com/p/classic-tee');
    await expect(page.getByRole('heading', { name: /classic tee/i })).toBeVisible();
  });

  await test.step('add to cart', async () => {
    await page.getByRole('button', { name: 'Add to cart' }).click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
  });

  await test.step('open cart and checkout', async () => {
    await page.getByRole('link', { name: 'Cart' }).click();
    await page.getByRole('button', { name: 'Checkout' }).click();
  });

  await test.step('enter shipping details', async () => {
    await page.getByLabel('Email').fill('qa@example.com');
    await page.getByLabel('Full name').fill('QA Runner');
    await page.getByLabel('Address').fill('1 Test Street');
    await page.getByRole('button', { name: 'Continue to payment' }).click();
  });

  await test.step('confirm order', async () => {
    await expect(page.getByText(/order summary/i)).toBeVisible();
  });
});

TEMPLATE 06API endpoint probe

Uses page.request to call a JSON endpoint directly, without rendering HTML. Faster than a UI test and useful for backends you want to check behind Hyperping’s probe regions.

06-api-probe.spec.tsTS
import { test, expect } from '@playwright/test';

test('public status API is healthy', async ({ request }) => {
  const res = await request.get('https://api.example.com/v1/status', {
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  });

  expect(res.status()).toBe(200);
  expect(res.headers()['content-type']).toContain('application/json');

  const body = await res.json();
  expect(body).toMatchObject({
    status: 'ok',
    version: expect.any(String),
  });
  expect(body.uptime_seconds).toBeGreaterThan(0);
});

TEMPLATE 07Broken-link audit

Extracts every nav href, issues HEAD-style GETs, and fails on the first 4xx or 5xx. A lightweight way to catch dead blog links, rotated CDN paths, or accidental release of draft content.

07-broken-links.spec.tsTS
import { test, expect } from '@playwright/test';

test('no broken links in the main navigation', async ({ page, request }) => {
  await page.goto('https://example.com');

  const hrefs = await page.locator('nav a').evaluateAll(
    links => links.map(a => a.href).filter(h => h.startsWith('http'))
  );

  const broken = [];
  for (const url of hrefs) {
    const res = await request.get(url, { failOnStatusCode: false });
    if (res.status() >= 400) broken.push(`${res.status()} ${url}`);
  }

  expect(broken, `broken links: \n${broken.join('\n')}`).toEqual([]);
});

TEMPLATE 08Performance budget

Reads PerformanceNavigationTiming from the page and fails when TTFB, DOMContentLoaded, or load exceed a budget. Logs the numbers on every run so you can watch them trend in the run logs.

08-perf-budget.spec.tsTS
import { test, expect } from '@playwright/test';

test('homepage meets performance budget', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'load' });

  const timing = await page.evaluate(() => {
    const [nav] = performance.getEntriesByType('navigation');
    return {
      ttfb: nav.responseStart - nav.requestStart,
      domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
      loadEvent: nav.loadEventEnd - nav.startTime,
    };
  });

  console.log('perf', timing);
  expect(timing.ttfb).toBeLessThan(800);
  expect(timing.domContentLoaded).toBeLessThan(2500);
  expect(timing.loadEvent).toBeLessThan(4000);
});

TEMPLATE 09Visual snapshot

Pixel-compares a single critical region against a golden snapshot. Waits for document.fonts.ready and disables animations so the diff does not flicker between runs.

09-visual.spec.tsTS
import { test, expect } from '@playwright/test';

test('hero section matches golden snapshot', async ({ page }) => {
  await page.goto('https://example.com');
  await page.evaluate(() => document.fonts.ready);

  const hero = page.getByTestId('hero');
  await expect(hero).toHaveScreenshot('hero.png', {
    maxDiffPixelRatio: 0.02,
    animations: 'disabled',
  });
});

TEMPLATE 10Mobile viewport

Uses Playwright’s devices preset to emulate an iPhone, including viewport, user agent, and touch events. Catches responsive regressions that desktop runs never see.

10-mobile.spec.tsTS
import { test, expect, devices } from '@playwright/test';

test.use({ ...devices['iPhone 14'] });

test('mobile nav opens and navigates', async ({ page }) => {
  await page.goto('https://example.com');

  await page.getByRole('button', { name: 'Open menu' }).tap();
  await expect(page.getByRole('navigation')).toBeVisible();

  await page.getByRole('link', { name: 'Pricing' }).tap();
  await expect(page).toHaveURL(/\/pricing/);
});

TEMPLATE 11Authenticated via storage state

Injects a session cookie from an environment variable so the check never logs in. Faster, less brittle against auth provider changes, and easier to rotate than a scripted login.

11-storage-state.spec.tsTS
import { test, expect } from '@playwright/test';

test.use({
  storageState: {
    cookies: [
      {
        name: 'session',
        value: process.env.SESSION_COOKIE,
        domain: 'app.example.com',
        path: '/',
        httpOnly: true,
        secure: true,
        sameSite: 'Lax',
        expires: -1,
      },
    ],
    origins: [],
  },
});

test('dashboard renders for an authenticated user', async ({ page }) => {
  await page.goto('https://app.example.com/dashboard');

  await expect(page.getByTestId('workspace-name')).toBeVisible();
  await expect(page.getByRole('navigation')).toContainText('Settings');
});

TEMPLATE 12File download

Waits for the download event, inspects the filename, and reads the stream to confirm the file is not empty. Covers invoice, report, and export flows that tend to silently break.

12-download.spec.tsTS
import { test, expect } from '@playwright/test';

test('invoice PDF downloads successfully', async ({ page }) => {
  await page.goto('https://app.example.com/billing');

  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.getByRole('button', { name: 'Download latest invoice' }).click(),
  ]);

  expect(download.suggestedFilename()).toMatch(/invoice.*\.pdf$/i);

  const stream = await download.createReadStream();
  let bytes = 0;
  for await (const chunk of stream) bytes += chunk.length;
  expect(bytes).toBeGreaterThan(1024);
});

Good habits across every template

  • Prefer role and label locators over CSS. getByRole('button', { name: 'Save' }) survives a stylesheet rewrite; .btn-primary.save does not.
  • Keep secrets in environment variables. Reference them with process.env.NAME so they never appear in the script body.
  • Stay under the 2-minute global timeout. Split long journeys into multiple checks rather than one mega-test, or call test.setTimeout explicitly.
  • Let the browser settle before asserting. await expect(locator).toBeVisible() already retries, so most waitForTimeout calls are unnecessary.
  • Log the values you care about. A console.log for latency, counts, or IDs shows up in run logs and makes post-mortem analysis far easier.

Where to go next