{
  "data_version": "1.0.0",
  "seed": 20261102,
  "tests": [
    {
      "id": "shop",
      "group": "Shop & pagination",
      "title": "The catalogue",
      "page": "/shop/",
      "answers": "/shop/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Categories, subcategories, numbered pages and next links, full product pages with JSON-LD, and the traps real shops set: tracking parameters, missing trailing slashes, products listed twice, items out of stock."
    },
    {
      "id": "infinite-scroll",
      "group": "JavaScript pages",
      "title": "Infinite scroll",
      "page": "/js/infinite-scroll/",
      "answers": "/js/infinite-scroll/answers.json",
      "difficulty": "Medium",
      "needs": "Needs JS",
      "what_it_tests": "Ninety-six items arrive twelve at a time as you scroll. Stop scrolling early and you miss some."
    },
    {
      "id": "load-more",
      "group": "JavaScript pages",
      "title": "Load more button",
      "page": "/js/load-more/",
      "answers": "/js/load-more/answers.json",
      "difficulty": "Medium",
      "needs": "Needs JS",
      "what_it_tests": "Sixty items behind a button that has to be clicked five times, waiting each time for the new rows."
    },
    {
      "id": "api",
      "group": "JavaScript pages",
      "title": "Data from a JSON API",
      "page": "/js/api/",
      "answers": "/js/api/answers.json",
      "difficulty": "Easy",
      "needs": "Needs JS",
      "what_it_tests": "An empty table the page fills from a JSON request after load. Render it, or find the API."
    },
    {
      "id": "shadow-dom",
      "group": "JavaScript pages",
      "title": "Shadow DOM",
      "page": "/js/shadow-dom/",
      "answers": "/js/shadow-dom/answers.json",
      "difficulty": "Hard",
      "needs": "Needs JS",
      "what_it_tests": "Reviews inside an open shadow root. document.querySelector can't see them; your selectors have to pierce it."
    },
    {
      "id": "iframe",
      "group": "JavaScript pages",
      "title": "Same-origin iframe",
      "page": "/js/iframe/",
      "answers": "/js/iframe/answers.json",
      "difficulty": "Medium",
      "needs": "Needs JS",
      "what_it_tests": "A store list that lives in an iframe, rendered by script inside the frame."
    },
    {
      "id": "spa",
      "group": "JavaScript pages",
      "title": "Client-rendered app",
      "page": "/js/spa/",
      "answers": "/js/spa/answers.json",
      "difficulty": "Hard",
      "needs": "Needs JS",
      "what_it_tests": "A single-page app that changes the URL with pushState. Every item has its own address, and none of them exists as HTML."
    },
    {
      "id": "lazy-images",
      "group": "JavaScript pages",
      "title": "Lazy-loaded images",
      "page": "/js/lazy-images/",
      "answers": "/js/lazy-images/answers.json",
      "difficulty": "Medium",
      "needs": "Needs JS",
      "what_it_tests": "Forty pictures that show a placeholder until they scroll into view. The real address only appears then."
    },
    {
      "id": "hidden-json",
      "group": "Hidden data",
      "title": "JSON in a script tag",
      "page": "/hidden/json/",
      "answers": "/hidden/json/answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "Twenty product pages that show a name and \"Price loading…\". The price, stock and variants sit in a JSON script tag the visible HTML never repeats."
    },
    {
      "id": "graphql",
      "group": "Hidden data",
      "title": "GraphQL reviews",
      "page": "/hidden/graphql/",
      "answers": "/hidden/graphql/answers.json",
      "difficulty": "Hard",
      "needs": "Needs JS",
      "what_it_tests": "Fifty reviews from a GraphQL-style POST, ten at a time, with cursors. Render the page, or replay the request and follow endCursor."
    },
    {
      "id": "token",
      "group": "Hidden data",
      "title": "API token in the page",
      "page": "/hidden/token/",
      "answers": "/hidden/token/answers.json",
      "difficulty": "Medium",
      "needs": "Needs JS",
      "what_it_tests": "A table filled from an API that answers 403 unless you send the token an inline script on the page sets."
    },
    {
      "id": "rate-limit",
      "group": "Bad days",
      "title": "Rate limit",
      "page": "/bad/rate-limit/",
      "answers": "/bad/rate-limit/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Twenty-one pages that answer 429 with Retry-After once you pass twelve requests in a ten-second window."
    },
    {
      "id": "slow",
      "group": "Bad days",
      "title": "Slow pages",
      "page": "/bad/slow/",
      "answers": "/bad/slow/answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "Pages that take one, three and ten seconds to answer. A short timeout loses the slow one."
    },
    {
      "id": "flaky",
      "group": "Bad days",
      "title": "Pages that fail once",
      "page": "/bad/flaky/",
      "answers": "/bad/flaky/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Every fourth item answers 503 the first time you ask, then works. One page always fails."
    },
    {
      "id": "redirects",
      "group": "Bad days",
      "title": "Redirects",
      "page": "/bad/redirects/",
      "answers": "/bad/redirects/answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "A 301, 302, 307, 308 chain, a loop that never ends, and a meta refresh."
    },
    {
      "id": "soft-404",
      "group": "Bad days",
      "title": "Soft 404s",
      "page": "/bad/soft-404/",
      "answers": "/bad/soft-404/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Ten product links. Three answer 200 but say the product is gone; one is a real 404."
    },
    {
      "id": "redesign",
      "group": "Bad days",
      "title": "Redesign switch",
      "page": "/redesign/",
      "answers": "/redesign/answers.json",
      "difficulty": "Hard",
      "needs": "HTTP only",
      "what_it_tests": "The same fifty products in two layouts with every class renamed, plus a crossed-out price and a shipping charge to lure the wrong number."
    },
    {
      "id": "login",
      "group": "Forms & logins",
      "title": "Login and members area",
      "page": "/forms/login/",
      "answers": "/forms/login/answers.json",
      "difficulty": "Medium",
      "needs": "Needs cookies",
      "what_it_tests": "Log in with the credentials on the page, keep the session cookie, and read three members-only pages that bounce you to the login without it."
    },
    {
      "id": "csrf",
      "group": "Forms & logins",
      "title": "CSRF token",
      "page": "/forms/csrf/",
      "answers": "/forms/csrf/answers.json",
      "difficulty": "Medium",
      "needs": "Needs cookies",
      "what_it_tests": "A form whose hidden token must come back with the cookie it was issued with. Replay it wrong and you get a 403."
    },
    {
      "id": "search",
      "group": "Forms & logins",
      "title": "Search form",
      "page": "/forms/search/",
      "answers": "/forms/search/answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "A GET search with paged results. Build the query string and follow the pages."
    },
    {
      "id": "multi-step",
      "group": "Forms & logins",
      "title": "Multi-step form",
      "page": "/forms/multi-step/",
      "answers": "/forms/multi-step/answers.json",
      "difficulty": "Hard",
      "needs": "Needs cookies",
      "what_it_tests": "Three steps, each a POST, carrying state in a signed cookie, ending in a reference code you can check."
    },
    {
      "id": "postback",
      "group": "Forms & logins",
      "title": "Postback dropdowns",
      "page": "/forms/postback/",
      "answers": "/forms/postback/answers.json",
      "difficulty": "Hard",
      "needs": "HTTP only",
      "what_it_tests": "Category, then subcategory, then results, each step a POST of the whole form with a signed __STATE field you must send back untouched."
    },
    {
      "id": "http-auth",
      "group": "Access",
      "title": "HTTP authentication",
      "page": "/access/http-auth/",
      "answers": "/access/http-auth/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Basic auth with a 401 challenge, a bearer token, and a third area that answers 404 until you send credentials it never asks for."
    },
    {
      "id": "referer",
      "group": "Access",
      "title": "Referer check",
      "page": "/access/referer/",
      "answers": "/access/referer/answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "Twelve item pages that redirect you away unless the Referer header says you came from the listing."
    },
    {
      "id": "consent",
      "group": "Access",
      "title": "Consent wall",
      "page": "/access/consent/",
      "answers": "/access/consent/answers.json",
      "difficulty": "Medium",
      "needs": "Needs cookies",
      "what_it_tests": "Articles that redirect to a consent form until you post it and keep the cookie, and a page whose text sits under a cookie banner."
    },
    {
      "id": "soft-block",
      "group": "Access",
      "title": "Soft blocks",
      "page": "/access/soft-block/",
      "answers": "/access/soft-block/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Twenty item pages. Four answer 200 with a \"verify you are human\" page instead of the item. Count them as missing."
    },
    {
      "id": "downloads",
      "group": "Access",
      "title": "File downloads",
      "page": "/access/downloads/",
      "answers": "/access/downloads/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "A CSV and a PDF sent as attachments with filename*, a report you get by posting a form, and a JSON file. Check names, sizes and hashes."
    },
    {
      "id": "traps",
      "group": "Crawling",
      "title": "Crawler traps",
      "page": "/crawl/traps/",
      "answers": "/crawl/traps/answers.json",
      "difficulty": "Hard",
      "needs": "HTTP only",
      "what_it_tests": "Twelve articles, a session ID that mints ten new URLs on every page, and a calendar whose next link never runs out."
    },
    {
      "id": "links",
      "group": "Crawling",
      "title": "Hidden links",
      "page": "/crawl/links/",
      "answers": "/crawl/links/answers.json",
      "difficulty": "Hard",
      "needs": "Needs JS",
      "what_it_tests": "Five pages, each reachable one way only: a script-added link, JSON-LD, a clickable div, a Link header and a <base href>."
    },
    {
      "id": "robots-meta",
      "group": "Crawling",
      "title": "Robots meta and headers",
      "page": "/crawl/robots-meta/",
      "answers": "/crawl/robots-meta/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Eight pages marked noindex and nofollow in meta tags, X-Robots-Tag headers and rel=\"nofollow\" links. Index the right ones; follow the right links."
    },
    {
      "id": "sitemaps",
      "group": "Crawling",
      "title": "Sitemap index",
      "page": "/crawl/sitemaps/",
      "answers": "/crawl/sitemaps/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "A sitemap index with a plain child and a gzipped one, thirty pages between them, ten of which nothing links to."
    },
    {
      "id": "encoding",
      "group": "Messy content",
      "title": "Character encodings",
      "page": "/messy/encoding/",
      "answers": "/messy/encoding/answers.json",
      "difficulty": "Hard",
      "needs": "HTTP only",
      "what_it_tests": "Three pages where the bytes, the Content-Type header and the meta tag disagree about the encoding. Get the pound signs and curly quotes right."
    },
    {
      "id": "formats",
      "group": "Messy content",
      "title": "Mixed formats",
      "page": "/messy/formats/",
      "answers": "/messy/formats/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Thirty rows with prices in four locales, ratings only in CSS classes, stock only in words and dates six ways."
    },
    {
      "id": "tables",
      "group": "Messy content",
      "title": "Awkward tables",
      "page": "/messy/tables/",
      "answers": "/messy/tables/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "A timetable with rowspan, colspan, repeated header rows and a total row, and a second table built from divs."
    },
    {
      "id": "invisible",
      "group": "Messy content",
      "title": "Invisible characters",
      "page": "/messy/invisible/",
      "answers": "/messy/invisible/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Names and prices full of zero-width characters, soft hyphens and non-breaking spaces, with a hidden decoy digit inside each price."
    },
    {
      "id": "microdata",
      "group": "Messy content",
      "title": "Microdata and RDFa",
      "page": "/messy/microdata/",
      "answers": "/messy/microdata/answers.json",
      "difficulty": "Medium",
      "needs": "HTTP only",
      "what_it_tests": "Twenty products whose visible text is rounded and cut short. The exact values are in schema.org microdata and RDFa."
    },
    {
      "id": "robots",
      "group": "Manners",
      "title": "robots.txt",
      "page": "/robots-test/",
      "answers": "/robots-answers.json",
      "difficulty": "Easy",
      "needs": "HTTP only",
      "what_it_tests": "A linked area that robots.txt disallows. A polite scraper fetches none of it."
    }
  ]
}
