A test that runs once is a script. A test that runs every pull request, every night, every release, for the next two years — that is a test suite. The difference between the two is almost never the assertions. It is the selectors.
This page is about how QA Shop is built so that the second kind of test is the easy kind to write. The short version: every interactive element carries a stable data-testid, every public identifier in the URL is a slug or an order number rather than a UUID, and we audit testid renames the same way we audit API breaking changes. The long version is below — with three concrete selector tours through the parts of the site you will spend the most time automating against.
§ 01 · WHY IT MATTERS
Why selector strategy matters
The framework debate gets the most oxygen, and it matters least. Whether you write page.locator(...) in Playwright or driver.find_element(...) in Selenium, the engine that runs your test is not what makes it flaky a year from now. The selector you put inside that call is.
We know this from watching teams burn out maintaining test suites that were perfectly written. The frontend ships a redesign. A class name changes from btn-primary to cta-orange-large. Twelve hundred tests fail overnight, and someone spends a week running sed across the spec directory. Or — worse — the test is written against a URL that includes a UUID, and after the next reseed the URL changes, every navigation 404s, and the team has to re-fixture the whole suite.
Three patterns produce this kind of pain reliably. CSS classes break on visual refactors, because that is what classes are for — they reflect presentation, and presentation changes. UUIDs in URLs change on reseed and expose internal IDs to anyone scraping the site. ARIA roles are excellent in isolation but get brittle in dense interfaces where five buttons share role="button" and the only thing telling them apart is text that the next copy refresh will rewrite.
The hooks you write tests against need to be invariant under the changes that are routine in a real product. That is the whole job of the data-testid attribute.
§ 02 · THE CONTRACT
The data-testid contract
Every interactive element on QA Shop, and every display element our test scenarios assert on, has a data-testid. That is the contract. It is not aspirational; we treat any control without one as a release-blocking bug, and our audit harness fails the build when one disappears without an alias.
The naming convention is mechanical, which is what you want from a contract. Names are kebab-case. They are scoped by feature using a prefix that matches the page or surface — pdp- for the product detail page, cart- for the cart, checkout- for the checkout wizard, order- for orders, auth- for login and registration.
Inside a prefix, the rest of the name describes what the element does, not how it looks: pdp-add-to-cart rather than pdp-orange-button, cart-summary-total rather than cart-bottom-right-amount.
§ 03 · ANTI-PATTERNS
What not to use, with reasons
If you are coming to QA Shop from a tutorial that taught you to write page.click(".btn-primary"), three habits are worth unlearning before you write your first spec.
Tailwind generates hundreds of utility classes per page; the .bg-orange-500 you see today is the .bg-amber-600 your designer renames in the next polish pass, and there is no warning. Even bespoke component classes like .checkout-button are presentational and get reshuffled the moment a designer wants to A/B test the layout. Treat the class list as decorative.
QA Shop intentionally does not expose UUIDs in any public surface. A product page lives at /products/the-art-of-software-testing, not /products/3f0c2c52-...; an order lives at /orders/[orderNumber] with a stable, human-readable order number. The decision is documented in public-ids-and-order-numbers — the effect for your tests is that URLs are stable across reseeds and scrapers cannot enumerate by guessing IDs.
Roles are excellent for accessibility testing, and on standalone pages with one button per role they are fine. On a product detail page with seven role-button elements, you need a name to disambiguate — and matching by name means matching by copy. The next time marketing rewrites the CTA from "Add to cart" to "Add to bag", every assertion that relies on the name breaks. If a testid is on the element, prefer the testid.
§ 04 · THREE TOURS
Three concrete selector tours
Three tours through the site, three different shapes of test, the testids you would actually grab in each.
Product detail page
The detail page at /products/the-art-of-software-testing is one of our seed fixtures — the slug is stable across reseeds, and we use it ourselves when validating PDP changes. Outer wrapper: data-testid="page-pdp". The title and category live at data-testid="pdp-title" and data-testid="pdp-category". Price renders at data-testid="pdp-price". The image gallery is data-testid="pdp-gallery" with a primary data-testid="pdp-main-image" and a thumbnail strip at data-testid="pdp-thumbs".
The buy box has the quantity stepper (data-testid="pdp-qty-inc" and pdp-qty-dec with pdp-qty-value for the current value), the add-to-cart CTA on data-testid="pdp-add-to-cart", and a wishlist toggle at data-testid="pdp-wishlist". Tabs along the bottom — description, specifications, reviews — sit inside data-testid="pdp-tabs", and the reviews block exposes both an aggregate (pdp-review-summary, pdp-review-average, pdp-review-count) and a list (pdp-review-list).
Cart and checkout
After adding to cart, the cart page at /cart is wrapped in data-testid="page-cart". The line list is data-testid="cart-list", the running line counts are at data-testid="cart-item-count", and the summary card on the side carries totals: data-testid="cart-summary-subtotal", cart-summary-tax, cart-summary-shipping, and the headline data-testid="cart-summary-total".
The promo input is data-testid="cart-summary-promo-input" with cart-summary-promo-apply to submit and cart-summary-promo-status to read the result. The checkout entry point is data-testid="cart-summary-checkout" (and the older alias checkout-button still resolves on the cart page CTA). On the wizard at /checkout, the step container is data-testid="checkout-step-content", the summary panel is data-testid="checkout-summary", and any inline error renders at data-testid="checkout-error".
Orders
The orders list at /orders wraps in data-testid="page-orders", and each order card exposes data-testid="order-card-number" (the stable order number — see below), order-card-date, order-card-item-count, and order-card-total. Click into an order detail at /orders/[orderNumber] and you land on data-testid="page-order-detail".
Inside, the layout is data-testid="order-detail-layout", the order number reappears at order-detail-number, the status pill is data-testid="order-status-badge", and the timeline is data-testid="order-timeline". Side panels cover totals (order-totals), shipping address (order-shipping-address), and payment method (order-payment-method). Action buttons sit in data-testid="order-actions" with order-action-print, order-action-reorder, and order-action-cancel for the verbs you might want to assert against.
That is roughly what a Monday-morning suite looks like for a fresh QA Shop tester: pick a path through the site, list the testids on each page, and decide which ones you care about asserting.
§ 05 · PUBLIC IDS
Order numbers, slugs, and the public-ID contract
The shape of an identifier matters more than people give it credit for. QA Shop has two kinds of identifiers: internal UUIDs that live in the database and never leave the server, and public identifiers — slugs for products, order numbers for orders, public_id for anything that does not fit either category — that are stable, human-readable, and safe to put in tests.
Order numbers follow the shape ORD-XXXXX-XXX; product slugs are kebab-case derivations of the product name (the-art-of-software-testing, wireless-bluetooth-headphones-pro).
The two consequences for your tests are the ones that surprise people coming from sites where every URL is a UUID. The first is that re-seeding the database does not break your spec. A reseed will reset internal IDs, but the slugs and order numbers we generate are deterministic from the input data — so a Playwright test pointing at /products/the-art-of-software-testing on Monday hits the same product on Friday, even after the database has been wiped twice in between.
The second is that scrapers cannot enumerate by ID-guessing. Order numbers do not increment in a way that lets you walk forward through the table; slugs only resolve to products we actually intend to publish.
This is not framework-level magic. The migration that introduced the column is supabase/migrations/20260418_add_public_ids_products_orders.sql, and the rationale lives in the architecture decision log under public-ids-and-order-numbers. The practical effect, for someone writing tests: anywhere you would have hard-coded a UUID, hard-code a slug or an order number instead. The tests will outlive the database state.
§ 06 · WHERE NEXT
Where the testid catalog lives
The full list of testids — page by page, with the test scenarios we have already validated against each — is in the testing guide. That is the page to bookmark; this one is the why, that one is the what.
Two adjacent surfaces are useful in different ways. The test-tools dock is the live state inspector for cookies, JWT claims, and current cart contents. It is the fastest way to confirm a flow worked the way you thought it did, and it carries its own qa- prefixed testids that we deliberately keep out of the global selector pool.
The /recipes section, once published, will pair specific testids with full runnable spec files in each of the four supported frameworks — so when you find a flow you want to automate, the recipe will show you exactly how to wire the testids into a working test in your runner of choice. Longer-form practitioner content on framework comparisons and portfolio testing lives under /learn.
The help index and the getting-started guide cover the rest of the documentation surface, including the account model and the manual walkthrough we recommend before writing a single assertion.12
You can come back to this page any time. The "last verified" stamp at the bottom tells you when we last walked the contract ourselves; the testids it lists are the ones we run against in our own CI on every push.
Footnotes
-
Playwright Best Practices — https://playwright.dev/docs/best-practices (verified 2026-04-28, Playwright 1.58) ↩
-
Selenium WebDriver — Locator strategies — https://www.selenium.dev/documentation/webdriver/elements/locators/ (verified 2026-04-28) ↩
Frequently asked questions
Last verified: