# 08 QA TEST PLAN

Nothing launches until every item passes. Run it on real devices: one low-end Android (Redmi or Infinix class) on 4G, one iPhone, Chrome desktop. Widths 360, 390, 430, 768, 1024, 1440.

Mark each item pass, fail or blocked. A blocked item is a launch blocker until it is resolved or explicitly deferred in writing.

---

## A. Discovery

| # | Test | Expected |
|---|---|---|
| A1 | Homepage on throttled 4G | loads under 3s, hero visible, no shift as fonts and images arrive |
| A2 | Mobile nav drawer | opens and closes, all four parent categories reachable in two taps |
| A3 | Search "cotton bedsheet", "razai", "bedsheat" | exact and partial match return results, the misspelling may return nothing but the empty state shows category links, never a dead end |
| A4 | Category page pagination | 24 products, then Load More appends. `/page/2/` also loads server side with the next 24, URL is clean, canonical self-references, title carries the page suffix |
| A5 | Sort by each of the four options | reorders correctly and preserves active filters |
| A6 | Filter fabric plus size plus colour together | results narrow, active filters visible with individual clear buttons, count updates, URL is shareable and reproduces the same result |
| A7 | Filter combination with zero results | friendly empty state plus Clear all, not a blank grid |
| A8 | Out of stock product | visible on the grid with the badge, add to cart disabled on the product page, still indexable, schema availability is OutOfStock |
| A9 | Shop by size chips | each lands on a filtered listing with the size preselected |

## B. Product page

| # | Test | Expected |
|---|---|---|
| B1 | Gallery swipe on mobile, thumbnail click on desktop | main image changes, zero layout shift |
| B2 | Variable product: choose size then colour | price, image, SKU and stock all update |
| B3 | Choose an unavailable combination | add to cart disabled with the specific out-of-stock message, not a silent failure |
| B4 | Add to cart with no variation chosen | blocked with an inline message |
| B5 | Quantity input | rejects 0 and negatives, accepts up to the configured maximum |
| B6 | Buy Now | goes straight to checkout with the item in cart, does not duplicate an existing cart line |
| B7 | Product missing a spec field | that section hides itself, no empty heading renders |
| B8 | Sticky bottom bar | appears once the main CTA scrolls out, works, does not cover the footer links |
| B9 | Review by a purchaser vs a non-purchaser | the purchaser's approved review carries Verified Purchase, the non-purchaser's does not |
| B10 | COD and delivery card | shows the correct estimate range and the COD badge |

## C. Cart and checkout

| # | Test | Expected |
|---|---|---|
| C1 | Update quantity in cart | totals and shipping recalculate |
| C2 | Remove item | undo link works, empty state shows Continue shopping |
| C3 | Cart just below free shipping threshold | shipping fee shows plus the "add Rs X more" nudge |
| C4 | Cart at or above threshold | shipping shows Rs 0 |
| C5 | Coupon that would only cross the threshold after discount | free shipping does NOT apply, the threshold is computed post-discount |
| C6 | Guest checkout end to end | completes with no account, no registration prompt anywhere |
| C7 | Checkout with email blank | order succeeds, admin still receives the notification |
| C8 | Coupon: valid, expired, below minimum | each fails with a specific message naming the reason. Category restriction is deferred (D-21), do not test it. |
| C9 | COD order placed | thank-you page shows order number, delivery estimate and the COD amount |
| C10 | Order emails | customer and admin both arrive, in the inbox not spam, PKR formatted correctly |
| C11 | sessionStorage persistence | fill the form, navigate back, return: values are still there |
| C12 | Submit with errors | scrolls to and focuses the first invalid field, shows the summary line, button is not permanently greyed |

## D. Pakistani edge cases

Run every one of these. This section is where a generic WooCommerce build fails.

| # | Input | Expected |
|---|---|---|
| D1 | `03001234567` | accepted |
| D2 | `+923001234567` | accepted, normalised to `03001234567` |
| D3 | `0300-1234567` | accepted, normalised |
| D4 | `3001234567` | accepted and corrected, or rejected with a clear message. Not silently stored. |
| D5 | `0300123456` (10 digits) | rejected inline |
| D6 | `abcdefghijk` | rejected |
| D7 | `+441234567890` | rejected or flagged |
| D8 | 240 character transliterated address, for example "Makan Number 45-B, Gali Number 7, Mohallah Islampura, Near Jamia Masjid Ghousia, Sabzazar Scheme, Multan Road" | stored in full, renders without overflow in admin, on the packing slip and in the email |
| D9 | City not in the list, for example Kot Addu or Hangu | order is placeable, shipping calculates at the Pakistan-wide rate, `_bc_city_recognised` is false, verification triggers, admin sees the flag |
| D10 | COD order above the high threshold | created as Verification Required, appears in the filter, the customer sees the advance-payment message if that rule is configured |
| D11 | Double-tap Place Order | exactly one order created, button disables on first click |
| D12 | Refresh the thank-you page three times | one order, and the GA4 and Meta purchase events fire once, not four times |
| D13 | Same cart, same phone, twice within 10 minutes | both orders visible, the second carries the duplicate flag |
| D14 | Address of one character or only emoji | rejected |
| D15 | Quantity 99 of one item | allowed or capped by rule, total does not overflow the layout, verification triggers on quantity |
| D16 | Phone with a prior RTO | COD blocked or verification forced, per configuration |

## E. Admin

| # | Test | Expected |
|---|---|---|
| E1 | Create a simple product and a variable product with 4 sizes and 3 colours | publishes, renders correctly on the front end |
| E2 | Duplicate a product, edit price, publish | SKU is not duplicated |
| E3 | Bulk edit prices on 10 products | applies correctly |
| E4 | Upload a 5 MB, 4000px image | capped to 1600px by `big_image_size_threshold`, our four sizes generated, WebP present, no fatal on the shared-host memory limit |
| E5 | Move an order through the full chain | each transition writes an order note with user and timestamp |
| E6 | Record verification status, date and notes | persists after save, visible to a Shop Manager |
| E7 | Verification list row | shows all ten fields without opening the order: ID, age in hours, name, tap-to-call phone, city, total, trigger badge, attempt count, prior order and RTO count, items in one line. Plus the three inline buttons. |
| E8 | WhatsApp button on a verification row | opens `wa.me` with the message pre-filled with order number, items, total and city |
| E9 | Assign courier and tracking number | appears in the shipped email and on the Track Order page |
| E10 | Cancel, RTO and Return an order | each is filterable, each is excluded from net sales, RTO records the courier charge |
| E11 | Record COD collected, then create a settlement covering 5 orders | all five link to the settlement, net received calculates, the orders leave the ageing report |
| E12 | Ageing report, both groups | an order delivered 22 days ago with COD collected but unsettled appears in group one. An order delivered 25 days ago with COD never marked collected appears in group two. |
| E13 | Dashboard numbers | cross-check against WooCommerce Analytics for the same day, they match |
| E14 | Log in as Shop Manager | no access to plugins, theme editor, users or settings, full access to products and orders |
| E15 | Approve, reject and delete a review | works, and the front end updates |
| E16 | Coupon with minimum order, expiry and usage limit | all three rules enforce at checkout |
| E17 | Order list with 500 orders | loads in under 3 seconds |

## F. Cross-cutting

| # | Test | Expected |
|---|---|---|
| F1 | Every page at 360px | no horizontal scroll |
| F2 | Screaming Frog crawl | zero 404s, zero redirect chains, canonicals self-reference except on filters |
| F3 | Rich Results Test | Product, BreadcrumbList and Organization all valid |
| F4 | Filter URL | returns `noindex, follow` and canonicals to the clean category, **and is not blocked in `robots.txt`**, or the crawler never sees the noindex (D-25) |
| F5 | GA4 DebugView and Meta Pixel Helper | every event fires once with correct PKR values, purchase fires exactly once per order |
| F6 | Meta Events Manager | purchase appears once, browser and Conversions API deduplicated by `event_id` |
| F7 | HTTPS | no mixed content anywhere |
| F8 | PageSpeed Insights, homepage and one product page | within the budget in `CLAUDE.md`, numbers recorded as the launch baseline |
| F9 | Keyboard only | gallery, accordion, drawer, filter panel and checkout all operable, focus ring always visible |
| F10 | `prefers-reduced-motion` | all transitions drop to near zero |
| F11 | Restore test | a full restore into a fresh staging database passes the ten-point check in the technical architecture, wall-clock time recorded |
| F12 | WooCommerce template version audit | no overridden template is more than one minor version behind |
| F13 | Every footer and mobile drawer link | resolves to a real page, zero dead links |
| F14 | Product feed | one row per variation, shared `item_group_id`, `identifier_exists: no`, category mapped per product type |
| F15 | CAPI phone hash | built from the `92...` form, verified in Meta's test tool, and it matches |
| F16 | Open Graph | product page preview renders with the right image, title and price |
| F17 | Customers list | totals for one phone match a hand-checked sum of that customer's orders |
| F18 | Password policy | a weak password is rejected on user creation and on reset |
