Skip to content
Independent guides for QA & test automationRSSEditorial policy
QA Vibes

Practice

Find the bugs your tests miss

A free, ad-free Playwright practice site: a shop with 10 seeded bugs. Run your Playwright tests against the correct shop and every buggy variant, paste the JSON report, and see which bugs your tests catch.

New to the lab? Start with 5 minutes, no setupWould your AI-generated tests catch these bugs? Try the free challenge →

How it works

  1. Write Playwright tests for the practice shop: log in, sort products, fill the cart, and check out. Log in as shopper with password practice-only.
  2. Use the config below. It runs your whole suite once against the correct shop (clean) and once against each of the 10 variants, v01 to v10. Each variant has exactly one bug.
  3. Run npx playwright test. Failures on the variants are expected: that's your tests finding bugs.
  4. Paste the results.json it writes into the scorer to see which bugs you caught.

What counts as a catch

A bug counts as caught when at least one test passes on the correct shop and fails on that variant. Passes and fails are what actually happened, so a test marked test.fail() that fails on the correct shop is a failure there. A test that fails on the correct shop is a false alarm, so it never counts. A flaky result, one that both passes and fails across retries or repeats, is shown but not counted. A variant where every test was skipped shows as not run.

A test can fail on a variant for a reason other than its bug. The scorer can't tell, so the hints and the answer key are there to check your reasoning. To practise telling the two apart, work through Why did this test pass?, three case files built from recorded runs on this shop. For bugs that live in the data instead of on a screen, try the SQL lab. To practise choosing test values before writing any code, answer test design practice questions.

Each bug belongs to one assertion habit, and several bugs share a habit. The scorer groups your result by habit, so a miss reads as what no test in your report did, not as a number out of ten. Where a habit's bugs didn't all run, it says so instead of guessing.

Every variant is checked on each change to this site: the correct shop must pass our reference tests, and each variant must fail exactly the reference test for its bug. A variant's behavior never changes; new bugs get new ids.

1. The config

The shop keeps its state in the browser, and each project starts with its variant in localStorage through storageState. If your own tests also use storageState, for example for a saved login, merge the two. Every project uses the same browser, so add devices if you want another one.

playwright.config.ts

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

const BASE_URL = "https://your-site.example";
const variants = ["clean", "v01", "v02", "v03", "v04", "v05", "v06", "v07", "v08", "v09", "v10"];

export default defineConfig({
  testDir: "./tests",
  retries: 0,
  reporter: [["list"], ["json", { outputFile: "results.json" }]],
  use: { baseURL: BASE_URL },
  // One project per variant. Each starts with the variant in localStorage.
  projects: variants.map((variant) => ({
    name: variant,
    use: {
      storageState: {
        cookies: [],
        origins: [{ origin: BASE_URL, localStorage: [{ name: "qa-hub-shop:variant", value: variant }] }],
      },
    },
  })),
});

2. Score your report

The report stays in your browser. Nothing is uploaded.

Explore a variant by hand

Opening the shop with ?variant= switches this browser to that variant until you switch back. The shop shows which variant is active, but not what's wrong with it.

Answer key

Try the hint before the answer. Knowing the bug makes it easy to write a test for that bug only; the useful skill is writing tests that would catch bugs you didn't know about.

  • v01HeaderExact values

    Hint

    Add more than one product and watch the cart in the header.

    Show the bug

    The cart badge stops at 1, however many products are in the cart.

  • v02CartWhich item, not how many

    Hint

    Put several products in the cart, then remove one that isn't first.

    Show the bug

    Remove deletes the first item in the cart, not the one you clicked.

  • v03ProductsThe whole ordered list

    Hint

    Try every sort order and read the prices in order.

    Show the bug

    Price (low to high) compares prices as text, so $8.99 sorts after $49.99.

  • v04Checkout overviewTotals recomputed

    Hint

    Buy more than one product and add up the prices yourself.

    Show the bug

    With two or more items, the subtotal drops the cents from every price.

  • v05Checkout overviewTotals recomputed

    Hint

    Check the tax with more than one product in the cart.

    Show the bug

    Tax is calculated on the first item only, not on the subtotal.

  • v06Checkout detailsInvalid states refused

    Hint

    Leave each required field empty in turn.

    Show the bug

    The postal code isn't required: checkout continues without one.

  • v07Checkout detailsExact values

    Hint

    Read the error message for each missing field.

    Show the bug

    A missing last name shows "Error: First name is required".

  • v08CartState after a reload

    Hint

    Reload the page at different points of your journey.

    Show the bug

    The cart empties when the page reloads. Moving between pages with the shop's own links keeps it.

  • v09CartInvalid states refused

    Hint

    Open the cart before adding anything.

    Show the bug

    Checkout is possible with an empty cart, and the order goes through with a total of $0.00.

  • v10AccessibilityAccessible names

    Hint

    Find each form field the way a screen reader would: by its label.

    Show the bug

    The postal code label isn't connected to its input, so the field has no accessible name.

Not one of the bugs

This site is built with Next.js, which adds a hidden element with role="alert" to announce page changes to screen readers. After you move between shop pages, getByRole("alert") matches that announcer as well as the shop's error message, and fails in strict mode even on the correct shop. Use getByTestId("error"), or narrow the locator with filter({ hasText }). Our own first test suite for this page hit exactly that, and many real Next.js apps behave the same way.

Limits

The bugs live in the shop's JavaScript, which your browser downloads, so reading the source reveals them. The scorer is for checking your own tests, not for proving anything to anyone else.

The practice pages never show ads, and the ad script isn't loaded on them, so no ad unit or pop-up can make a test fail for reasons unrelated to your code. If this site enables analytics, a small cookieless analytics script may load; it doesn't change the page. For more practice targets, including ones with deliberately unstable pages, see our QA engineer roadmap.