← All posts

What QA means for a website, and how I test before launch

Quality assurance for websites: what to check before launch, from forms and Core Web Vitals to accessibility, and how to automate it with Playwright and Lighthouse.

On this page

Most bugs on a website are not found by the person who built it. They are found by a customer on a phone, three weeks after launch, when the contact form quietly stops sending email. Quality assurance (QA) is the work that moves those discoveries to before the launch, where they are cheap to fix.

Here is what QA means for a website, what I check before a site goes live, and which parts are worth automating.

What QA is, and what it isn't

QA is the set of checks that answer one question: does the website do what it promises, for every visitor, on every device? That covers more than “does it look right on my laptop”:

  • Functionality: forms send, links work, navigation reaches every page.
  • Compatibility: the layout holds on small phones, large screens and the browsers your visitors actually use.
  • Performance: pages load fast on a mid-range phone and a mobile connection.
  • Accessibility: the site works with a keyboard and a screen reader.
  • SEO basics: titles, descriptions and structured data are present and correct.

QA is not a single test run at the end. Checks that only happen the day before launch tend to get skipped when the schedule is tight, which is exactly when they matter most.

Why it matters for a business website

A broken page on a small business site is rarely dramatic. Nothing crashes. A form fails, an enquiry never arrives, a page takes six seconds to load on mobile and the visitor goes back to the search results. You don't see those losses in an error log, only in fewer enquiries.

The most expensive bug is the one nobody reports.

What I check before a launch

Forms, links and navigation

Every form gets submitted for real, with valid and invalid input, and I confirm the message arrives where it should. Every link in the navigation, footer and content is followed. Redirects from old URLs are tested one by one.

Devices and browsers

I check the main templates at phone, tablet and desktop widths, in Chrome, Safari and Firefox. Safari on iOS gets extra attention: it is where most layout surprises come from.

Performance

I measure Core Web Vitals (LCP, INP and CLS) on the heaviest templates, with mobile throttling. Large images, render-blocking scripts and third-party tags are the usual suspects.

Accessibility

Keyboard navigation through the whole page, visible focus states, labelled form fields, sufficient contrast, alt text on images and one h1 per page. Automated tools catch about a third of accessibility issues; the rest needs a person with a keyboard.

SEO basics

Unique titles and meta descriptions, a canonical URL on every page, hreflang for multilingual sites, a sitemap, and a robots.txt that doesn't accidentally block the whole site.

Automating the repetitive parts

Manual checks find the surprising problems. Automated checks make sure the problems you already fixed stay fixed. These are the three I set up on most projects.

End-to-end tests with Playwright

Playwright drives a real browser through the site. I run the tests against the production build, on a desktop and a mobile viewport:

playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  use: { baseURL: "http://localhost:4321" },
  projects: [
    { name: "desktop", use: { ...devices["Desktop Chrome"] } },
    { name: "mobile", use: { ...devices["Pixel 7"] } },
  ],
  // Builds are tested as they ship: against `astro preview`, not the dev server.
  webServer: {
    command: "npm run build && npm run preview",
    url: "http://localhost:4321",
    reuseExistingServer: !process.env.CI,
  },
});

The tests themselves stay small. They check that every important page loads, has exactly one h1, and logs no console errors:

tests/pages.spec.ts
import { test, expect } from "@playwright/test";

const pages = ["/", "/about/", "/services/", "/projects/", "/de/"];

for (const path of pages) {
  test(`${path} has a title, one h1 and no console errors`, async ({ page }) => {
    const errors: string[] = [];
    page.on("console", (msg) => msg.type() === "error" && errors.push(msg.text()));

    const response = await page.goto(path);
    expect(response?.status()).toBe(200);
    await expect(page).toHaveTitle(/.+/);
    await expect(page.locator("h1")).toHaveCount(1);
    expect(errors).toEqual([]);
  });
}

test("the main navigation reaches every page", async ({ page }) => {
  await page.goto("/");
  await page.getByRole("link", { name: "Projects" }).first().click();
  await expect(page).toHaveURL(/\/projects\/$/);
});

Lighthouse budgets

Lighthouse CI runs Lighthouse against the built site and fails when a score drops below a threshold, so a heavy image or a new script can't slip through unnoticed:

lighthouserc.json
{
  "ci": {
    "collect": {
      "staticDistDir": "./dist",
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.95 }],
        "categories:seo": ["error", { "minScore": 0.95 }]
      }
    }
  }
}

Broken links

A link checker crawls the built pages and reports every link that returns an error:

bash
# Build first, then crawl every page in dist/ and report broken links.
npm run build
npx linkinator ./dist --recurse --verbosity error

Running it on every change

All of this runs on every pull request, so nothing reaches the live site without passing:

.github/workflows/qa.yml
name: QA

on: [pull_request]

jobs:
  qa:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - run: npx linkinator ./dist --recurse --verbosity error
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test
      - run: npx @lhci/cli autorun

A launch checklist

The short version I go through before every launch:

  1. Submit every form and confirm the message arrives.
  2. Click through the navigation and footer on a phone.
  3. Test the main templates in Safari on iOS.
  4. Check Core Web Vitals on the heaviest page.
  5. Tab through the page with the keyboard only.
  6. Check titles, descriptions, canonical URLs and the sitemap.
  7. Test redirects from the old site's most visited URLs.
  8. Confirm analytics and consent work as intended.

QA doesn't stop at launch

Content changes, plugins update, and a new marketing script appears in the header. The automated checks keep running on every change, and a quick manual pass after bigger updates catches what they can't. QA is less a phase of a project than a habit of the codebase.

If you want a second pair of eyes on your website before or after a launch, get in touch. I'll tell you what I'd check first.