@mohasinac/appkit 4.39.1 → 4.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/jobs/core/countersReconcile.js +21 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +35 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +17 -5
- package/dist/features/categories/repository/categories.repository.d.ts +65 -1
- package/dist/features/categories/repository/categories.repository.js +245 -77
- package/dist/features/categories/types/index.d.ts +2 -2
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +145 -0
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +234 -0
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +32 -0
- package/dist/features/tester/seed-data/authored/buying__cart.js +60 -0
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +138 -0
- package/dist/features/tester/seed-data/authored/buying__wishlist-history.js +30 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +145 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +154 -0
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +88 -0
- package/dist/features/tester/seed-data/authored/index.js +22 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +134 -7
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +89 -0
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +141 -0
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +115 -0
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +148 -0
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +118 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +69 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +471 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/tabs-overflow.js +30 -7
- package/package.json +1 -1
|
@@ -221,6 +221,31 @@ const rawTesterChecklistItems = [
|
|
|
221
221
|
},
|
|
222
222
|
]),
|
|
223
223
|
...group("buying", "Buying", [
|
|
224
|
+
{
|
|
225
|
+
/*
|
|
226
|
+
* The post-purchase money surface. `/user/orders/[id]/` has five
|
|
227
|
+
* subroutes — cancel, return, track, invoice, payment — and before
|
|
228
|
+
* 2026-09-14 only `track` had a single case anywhere in the catalogue.
|
|
229
|
+
*
|
|
230
|
+
* 🛑 NO SEEDED ORDER BELONGS TO THE BOT (measured: 0 of 64). That is why
|
|
231
|
+
* the positive cases buy something first and the borrowed fixture ids are
|
|
232
|
+
* used only as the ownership NEGATIVES — which is the dimension nothing
|
|
233
|
+
* tested at all, and the one where a mistake is an IDOR rather than a
|
|
234
|
+
* cosmetic bug.
|
|
235
|
+
*/
|
|
236
|
+
pageKey: "order-detail-actions",
|
|
237
|
+
pageLabel: "Order detail — cancel, return, track, invoice, payment",
|
|
238
|
+
href: "/user/orders",
|
|
239
|
+
cases: [
|
|
240
|
+
{ key: "invoice-downloads-for-owner", label: "The buyer can download the invoice for their own order, and it is a real PDF" },
|
|
241
|
+
{ key: "invoice-refuses-other-buyer", label: "The invoice URL for somebody else's order is refused, not served" },
|
|
242
|
+
{ key: "cancel-page-refuses-delivered-order", label: "The cancel page refuses an order that is already delivered, and says why" },
|
|
243
|
+
{ key: "return-request-round-trip", label: "Requesting a return moves the order to Return Requested and survives a reload" },
|
|
244
|
+
{ key: "track-shows-real-dates", label: "The tracking timeline shows the order's REAL dates, not invented or blank ones" },
|
|
245
|
+
{ key: "order-subroutes-404-for-other-users-order", label: "Every /user/orders/[id]/* subroute 404s for an order the signed-in user does not own" },
|
|
246
|
+
{ key: "order-subroutes-redirect-guest", label: "A signed-out visitor hitting an order subroute is sent to sign in, never shown the order" },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
224
249
|
{
|
|
225
250
|
pageKey: "browsing-search",
|
|
226
251
|
pageLabel: "Browsing & Search",
|
|
@@ -699,6 +724,24 @@ const rawTesterChecklistItems = [
|
|
|
699
724
|
description: "Fixed 2026-08-20 — the Razorpay pre-charge amount (computed before opening the payment modal) omitted the seller's shipping fee entirely, while the order created afterward recorded a higher total that included it, so the buyer was silently charged less via the gateway than what got recorded as owed. Only testable when Site Settings → Payments → Razorpay is enabled and the seller has a configured shipping fee. Compare the amount shown in the Razorpay modal against the order's Total on the confirmation page — they should match exactly.",
|
|
700
725
|
href: "/checkout",
|
|
701
726
|
},
|
|
727
|
+
/*
|
|
728
|
+
* SIGNED-OUT checkout. 30 cases existed and NONE was signed-out, so the
|
|
729
|
+
* refusal path — the single most important thing checkout does for a guest
|
|
730
|
+
* — had no coverage at all. Only roles exactly ["guest"] reaches the guest
|
|
731
|
+
* identity slice.
|
|
732
|
+
*/
|
|
733
|
+
{
|
|
734
|
+
key: "checkout-guest-redirected-to-signin",
|
|
735
|
+
label: "A signed-out visitor opening /checkout directly is sent to sign in, and is not shown a checkout form",
|
|
736
|
+
description: "The failure to look for is a checkout that renders its steps for a signed-out visitor and only refuses at the end, after they have typed an address.",
|
|
737
|
+
href: "/checkout",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
key: "checkout-guest-returns-after-signin",
|
|
741
|
+
label: "After signing in from that prompt the visitor lands back on checkout with their cart intact, not on the homepage with an empty cart",
|
|
742
|
+
description: "A redirect that forgets where it came from is the same as losing the sale. The guest cart must have merged into the account cart by this point.",
|
|
743
|
+
href: "/checkout",
|
|
744
|
+
},
|
|
702
745
|
],
|
|
703
746
|
},
|
|
704
747
|
{
|
|
@@ -919,6 +962,22 @@ const rawTesterChecklistItems = [
|
|
|
919
962
|
{ key: "history-revisit-reorders", label: "Re-visiting a product already in history removes the old entry and moves it to the front, rather than duplicating it" },
|
|
920
963
|
{ key: "history-fifo-cap", label: "History silently evicts the oldest entry once more than 50 items have been viewed (no error shown to the user)" },
|
|
921
964
|
{ key: "history-guest-merge-on-login", label: "Guest browsing history (localStorage) merges correctly with the account's history after login, deduplicated by product" },
|
|
965
|
+
/*
|
|
966
|
+
* SIGNED-OUT wishlist. 20 cases existed, 2 signed-out, and both were about
|
|
967
|
+
* the login MERGE rather than about what a guest sees before logging in.
|
|
968
|
+
*/
|
|
969
|
+
{
|
|
970
|
+
key: "wishlist-guest-heart-prompts-signin",
|
|
971
|
+
label: "A signed-out visitor tapping the heart on product-beyblade-burst-valkyrie is asked to sign in, and the heart does not appear to have saved",
|
|
972
|
+
description: "The gate must not optimistically fill the heart before refusing — a filled heart that saved nothing is worse than a refusal, because the visitor believes the item is kept.",
|
|
973
|
+
href: "/products/product-beyblade-burst-valkyrie",
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
key: "wishlist-guest-page-signed-out",
|
|
977
|
+
label: "A signed-out visitor opening /wishlist is asked to sign in rather than shown a permanently empty wishlist",
|
|
978
|
+
description: "An empty-state that reads \"nothing saved yet\" to a signed-out visitor is indistinguishable from a wishlist whose contents were lost.",
|
|
979
|
+
href: "/wishlist",
|
|
980
|
+
},
|
|
922
981
|
],
|
|
923
982
|
},
|
|
924
983
|
{
|
|
@@ -1292,6 +1351,35 @@ const rawTesterChecklistItems = [
|
|
|
1292
1351
|
description: "Hardened 2026-08-21. The add-on booleans used to be accepted in the checkout request body as well as being stored per-store on the cart, so two sources could disagree about what the buyer had actually selected — and the cart's selection is the only one the buyer ever saw. The request body no longer carries them at all. To test: tick gift wrap for seller A only, go to checkout and note the Total, press Back to the cart, then return to checkout — the same single gift-wrap fee must still be there, for seller A only, with an unchanged Total. Repeat having ticked nothing and confirm no add-on line ever appears.",
|
|
1293
1352
|
href: "/cart",
|
|
1294
1353
|
},
|
|
1354
|
+
/*
|
|
1355
|
+
* SIGNED-OUT cart. Only a case whose `roles` is exactly ["guest"] is routed
|
|
1356
|
+
* into the guest identity slice, which copies an EMPTY storage state over
|
|
1357
|
+
* session.json — a two-role case runs signed in and therefore cannot test
|
|
1358
|
+
* any of this. 71 cart cases existed and 3 were signed-out.
|
|
1359
|
+
*/
|
|
1360
|
+
{
|
|
1361
|
+
key: "cart-guest-add-and-persist",
|
|
1362
|
+
label: "A signed-out visitor can add product-beyblade-burst-valkyrie to the cart, and it survives a full page reload",
|
|
1363
|
+
description: "The guest cart is localStorage-backed. If it does not survive a reload the visitor loses the cart between any two navigations, which is invisible to a signed-in tester.",
|
|
1364
|
+
href: "/cart",
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
key: "cart-guest-prices-gated",
|
|
1368
|
+
label: "A signed-out visitor sees \"Sign in to see price\" rather than an amount on the cart line and in the summary",
|
|
1369
|
+
description: "Prices are gated for guests. The gate has three states and the middle one is a neutral placeholder, so the failure to look for is a FLASH of the sign-in prompt at a signed-in buyer, and a permanent amount for a signed-out one.",
|
|
1370
|
+
href: "/cart",
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
key: "cart-guest-checkout-prompts-signin",
|
|
1374
|
+
label: "A signed-out visitor pressing the cart's checkout CTA is asked to sign in rather than being dropped on an empty checkout",
|
|
1375
|
+
href: "/cart",
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
key: "cart-guest-group-line-refused",
|
|
1379
|
+
label: "A signed-out visitor is told to sign in when adding a grouped/bundle line, rather than the line half-appearing",
|
|
1380
|
+
description: "Group lines cannot exist in the guest cart — it is keyed {productId, quantity} only and a group line could not survive the login merge. The refusal must be explicit.",
|
|
1381
|
+
href: "/cart",
|
|
1382
|
+
},
|
|
1295
1383
|
],
|
|
1296
1384
|
},
|
|
1297
1385
|
{
|
|
@@ -2100,6 +2188,62 @@ const rawTesterChecklistItems = [
|
|
|
2100
2188
|
},
|
|
2101
2189
|
]),
|
|
2102
2190
|
...group("selling", "Selling", [
|
|
2191
|
+
{
|
|
2192
|
+
/*
|
|
2193
|
+
* The UPDATE path. The create path has 57 cases on /store/products/new
|
|
2194
|
+
* alone and this had ZERO — which is precisely where the 2026-09-14
|
|
2195
|
+
* data-loss bug lived (Root Cause #98): nine editors spread an
|
|
2196
|
+
* ActionResult envelope as a product, so the form opened blank and Save
|
|
2197
|
+
* wrote "draft" over a live listing.
|
|
2198
|
+
*/
|
|
2199
|
+
pageKey: "listing-edit-roundtrip",
|
|
2200
|
+
pageLabel: "Editing an Existing Listing (round-trip)",
|
|
2201
|
+
href: "/store/products",
|
|
2202
|
+
cases: [
|
|
2203
|
+
{ key: "edit-opens-populated", label: "Opening a published listing for edit shows its REAL saved values, not an empty form" },
|
|
2204
|
+
{ key: "edit-category-preselected", label: "The Category field is pre-selected with the listing's actual category, not blank" },
|
|
2205
|
+
{ key: "edit-save-keeps-published", label: "Saving an edit to a PUBLISHED listing leaves it published — it must not drop to draft" },
|
|
2206
|
+
{ key: "edit-missing-id-404s", label: "Opening an edit URL for a listing id that does not exist shows 404, not a blank form" },
|
|
2207
|
+
{ key: "edit-other-sellers-listing-404s", label: "Opening another seller's listing id for edit shows 404" },
|
|
2208
|
+
{ key: "edit-ancestor-pages-after-recategorise", label: "Re-categorising a listing makes it appear on the new category's page AND on every ancestor of it" },
|
|
2209
|
+
],
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
/*
|
|
2213
|
+
* The digital-content pool. Zero cases before 2026-09-14, and the pool's
|
|
2214
|
+
* only writer answered 501 — so every digital-code purchase delivered
|
|
2215
|
+
* nothing, silently, with the order completing normally (Root Cause #103).
|
|
2216
|
+
*/
|
|
2217
|
+
pageKey: "digital-content-delivery",
|
|
2218
|
+
pageLabel: "Digital Content — codes, QR images and files",
|
|
2219
|
+
href: "/store/digital-codes",
|
|
2220
|
+
cases: [
|
|
2221
|
+
{ key: "pool-empty-by-default", label: "A new digital-code listing shows an EMPTY pool, and warns that an empty pool delivers nothing" },
|
|
2222
|
+
{ key: "add-codes-bulk", label: "Pasting several codes adds them all, de-duplicates the paste, and updates the available count" },
|
|
2223
|
+
{ key: "buy-then-reveal-code", label: "Buying a digital-code listing and clicking Reveal shows a real code from the pool" },
|
|
2224
|
+
{ key: "available-count-drops-after-purchase", label: "The listing's available count DROPS by one after a purchase" },
|
|
2225
|
+
{ key: "claimed-entry-cannot-be-removed", label: "An entry already delivered to a buyer has no Remove button, and the API refuses to delete it" },
|
|
2226
|
+
{ key: "asset-download-requires-ownership", label: "The delivered asset URL refuses a guest and a non-buyer, and serves the buyer" },
|
|
2227
|
+
{ key: "seller-cannot-upload-non-image", label: "A seller may attach a QR image but not an arbitrary file; an admin may attach either" },
|
|
2228
|
+
{ key: "pool-list-never-shows-the-code", label: "The seller's pool list shows status and kind but never the code itself" },
|
|
2229
|
+
],
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
/*
|
|
2233
|
+
* The seller's own order list, at a store large enough to break it. It
|
|
2234
|
+
* queried `productId in [...]` against Firestore's 30-value cap, so it
|
|
2235
|
+
* was a permanent 500 for the only real seller (65 products) and fine for
|
|
2236
|
+
* every small store — which is why it survived.
|
|
2237
|
+
*/
|
|
2238
|
+
pageKey: "seller-orders-at-scale",
|
|
2239
|
+
pageLabel: "Seller orders — at a store with many listings",
|
|
2240
|
+
href: "/store/orders",
|
|
2241
|
+
cases: [
|
|
2242
|
+
{ key: "orders-list-loads-for-large-store", label: "The order list loads for a store with more than 30 listings" },
|
|
2243
|
+
{ key: "dashboard-revenue-not-zero", label: "The store dashboard shows real order counts and revenue, not zeros" },
|
|
2244
|
+
{ key: "orders-only-this-store", label: "The list contains only this store's orders" },
|
|
2245
|
+
],
|
|
2246
|
+
},
|
|
2103
2247
|
{
|
|
2104
2248
|
pageKey: "become-seller",
|
|
2105
2249
|
pageLabel: "Become a Seller & Store Setup",
|
|
@@ -2862,6 +3006,67 @@ const rawTesterChecklistItems = [
|
|
|
2862
3006
|
},
|
|
2863
3007
|
]),
|
|
2864
3008
|
...group("content-discovery", "Content & Discovery", [
|
|
3009
|
+
{
|
|
3010
|
+
/*
|
|
3011
|
+
* Event DETAIL. All 29 existing event cases stop at the index, while
|
|
3012
|
+
* /events/[id]/ has four subroutes — participate, leaderboard, winner,
|
|
3013
|
+
* spin-results — with none at all. Several real bugs were found on these
|
|
3014
|
+
* pages by hand during the 2026-09 run, which is the usual consequence of
|
|
3015
|
+
* a surface nobody has a procedure for.
|
|
3016
|
+
*/
|
|
3017
|
+
pageKey: "event-detail-subroutes",
|
|
3018
|
+
pageLabel: "Event detail — participate, leaderboard, winner, spin results",
|
|
3019
|
+
href: "/events",
|
|
3020
|
+
cases: [
|
|
3021
|
+
{ key: "detail-tabs-match-event-type", label: "An event's tabs match its TYPE — a poll shows no spin results, a sale shows no leaderboard" },
|
|
3022
|
+
{ key: "participate-records-an-entry", label: "Participating records an entry that is still there after a reload" },
|
|
3023
|
+
{ key: "participate-twice-is-refused", label: "Participating a second time is refused with a reason, not silently duplicated" },
|
|
3024
|
+
{ key: "leaderboard-ranks-by-a-real-number", label: "The leaderboard ranks by a real score and shows the same entry count the event reports" },
|
|
3025
|
+
{ key: "winner-page-before-draw", label: "The winner page on an undrawn event says so — it must not show a blank or a fabricated winner" },
|
|
3026
|
+
{ key: "winner-page-after-draw", label: "An ended raffle's winner page names the real winner recorded on the event" },
|
|
3027
|
+
{ key: "cancelled-event-refuses-participation", label: "A cancelled event cannot be participated in, and says why" },
|
|
3028
|
+
{ key: "guest-sees-event-but-is-prompted", label: "A signed-out visitor can read an event but is prompted to sign in to take part" },
|
|
3029
|
+
],
|
|
3030
|
+
},
|
|
3031
|
+
{
|
|
3032
|
+
/*
|
|
3033
|
+
* Category counts and the LISTING they promise. A category's number is its
|
|
3034
|
+
* own items plus every descendant's, and clicking it must list exactly
|
|
3035
|
+
* those items. Both halves were wrong: the nightly reconciler could not
|
|
3036
|
+
* express own-vs-rollup (Root Cause #102), and the chips showed own while
|
|
3037
|
+
* the header showed rollup.
|
|
3038
|
+
*/
|
|
3039
|
+
pageKey: "category-counts-and-rollup",
|
|
3040
|
+
pageLabel: "Category counts and the listing they promise",
|
|
3041
|
+
href: "/categories",
|
|
3042
|
+
cases: [
|
|
3043
|
+
{ key: "leaf-count-matches-its-listing", label: "A leaf category's count equals the number of products its own page lists" },
|
|
3044
|
+
{ key: "parent-count-includes-descendants", label: "A parent's count is its own items PLUS every descendant's, not just its own" },
|
|
3045
|
+
{ key: "parent-listing-includes-descendant-items", label: "Opening a parent category LISTS the descendants' items too, not only its own" },
|
|
3046
|
+
{ key: "header-and-child-chips-agree", label: "The header count and the child chips beneath it are the same kind of number and compose" },
|
|
3047
|
+
{ key: "root-count-is-the-whole-subtree", label: "A root category's count equals the sum over its whole subtree" },
|
|
3048
|
+
{ key: "sibling-isolation", label: "A sibling branch's items are NOT counted in the other sibling" },
|
|
3049
|
+
{ key: "guest-sees-the-same-counts", label: "A signed-out visitor sees the same category counts as a signed-in one" },
|
|
3050
|
+
{ key: "brand-count-matches-its-listing", label: "A brand page's count equals what that brand page lists" },
|
|
3051
|
+
],
|
|
3052
|
+
},
|
|
3053
|
+
{
|
|
3054
|
+
/*
|
|
3055
|
+
* Store reviews. The endpoint answered 200 with zero reviews for EVERY
|
|
3056
|
+
* store while 79 approved ones existed, because it ordered products by a
|
|
3057
|
+
* field no product has (Root Cause #100) — and once it returned rows it
|
|
3058
|
+
* returned the raw document, ciphertext name and all.
|
|
3059
|
+
*/
|
|
3060
|
+
pageKey: "store-reviews-aggregate",
|
|
3061
|
+
pageLabel: "Store reviews tab — aggregate and privacy",
|
|
3062
|
+
href: "/stores",
|
|
3063
|
+
cases: [
|
|
3064
|
+
{ key: "reviews-tab-not-empty", label: "A store with reviews shows them on its Reviews tab — the tab must not read as empty" },
|
|
3065
|
+
{ key: "average-matches-the-rows", label: "The average rating and the star distribution agree with the reviews listed" },
|
|
3066
|
+
{ key: "reviewer-name-is-masked", label: "A reviewer's name is masked, and no encrypted value or internal field is visible" },
|
|
3067
|
+
{ key: "review-search-matches-body-text", label: "Searching the reviews matches a word from a review's BODY, and a nonsense term returns none" },
|
|
3068
|
+
],
|
|
3069
|
+
},
|
|
2865
3070
|
{
|
|
2866
3071
|
pageKey: "blog",
|
|
2867
3072
|
pageLabel: "Blog",
|
|
@@ -3928,6 +4133,53 @@ const rawTesterChecklistItems = [
|
|
|
3928
4133
|
},
|
|
3929
4134
|
]),
|
|
3930
4135
|
...group("public-pages", "Public & Marketing Pages", [
|
|
4136
|
+
{
|
|
4137
|
+
/*
|
|
4138
|
+
* The six browse pages wired into the footer on 2026-09-14. Each shipped
|
|
4139
|
+
* with a real page and a real ROUTES.PUBLIC constant and NO link from
|
|
4140
|
+
* anywhere — reachable only by typing the URL — so none has ever had a
|
|
4141
|
+
* case (Root Cause #37).
|
|
4142
|
+
*
|
|
4143
|
+
* Every one gets a guest case: a signed-out visitor is their commonest
|
|
4144
|
+
* caller, and /products' own scoping rules mean an out-of-span type chip
|
|
4145
|
+
* silently WIDENS results rather than narrowing them.
|
|
4146
|
+
*/
|
|
4147
|
+
pageKey: "newly-wired-browse-indexes",
|
|
4148
|
+
pageLabel: "Classifieds, Digital Codes, Live, Lotteries, Brands, Sellers",
|
|
4149
|
+
href: "/classified",
|
|
4150
|
+
cases: [
|
|
4151
|
+
{ key: "all-six-reachable-from-footer", label: "All six pages are reachable by clicking the footer, not only by typing the URL" },
|
|
4152
|
+
{ key: "classified-lists-only-classifieds", label: "/classified lists classified listings and nothing of another type" },
|
|
4153
|
+
{ key: "digital-codes-lists-only-digital", label: "/digital-codes lists digital-code listings and nothing of another type" },
|
|
4154
|
+
{ key: "live-lists-only-live", label: "/live lists live-item listings and nothing of another type" },
|
|
4155
|
+
{ key: "lottery-lists-active-lotteries", label: "/lottery lists active lottery events with their slot counts" },
|
|
4156
|
+
{ key: "brands-tiles-open-their-brand", label: "Each /brands tile opens that brand's page and lists that brand's products" },
|
|
4157
|
+
{ key: "sellers-is-not-a-copy-of-stores", label: "/sellers shows verified sellers and is visibly a different page from /stores" },
|
|
4158
|
+
{ key: "guest-can-browse-all-six", label: "A signed-out visitor can browse all six, with prices gated rather than the page broken" },
|
|
4159
|
+
],
|
|
4160
|
+
},
|
|
4161
|
+
{
|
|
4162
|
+
/*
|
|
4163
|
+
* The store's own tabs. Nine of them are untested, starting with
|
|
4164
|
+
* `products` — which is the DEFAULT tab, i.e. the first thing anyone sees
|
|
4165
|
+
* after clicking a store.
|
|
4166
|
+
*
|
|
4167
|
+
* The reviews tab is deliberately included: it answered 200 with zero
|
|
4168
|
+
* reviews for every store until 2026-09-14 (Root Cause #100), and the
|
|
4169
|
+
* only symptom was a sentence that reads as a fact about the store.
|
|
4170
|
+
*/
|
|
4171
|
+
pageKey: "store-tabs",
|
|
4172
|
+
pageLabel: "Store page tabs — products, reviews, and the per-type tabs",
|
|
4173
|
+
href: "/stores/store-beyblade-arena",
|
|
4174
|
+
cases: [
|
|
4175
|
+
{ key: "default-tab-lists-products", label: "Opening a store lands on Products and lists that store's listings" },
|
|
4176
|
+
{ key: "each-tab-lists-only-its-type", label: "Each per-type tab lists only that listing type" },
|
|
4177
|
+
{ key: "empty-tab-says-empty", label: "A tab with nothing in it shows an empty state, not a spinner or a blank panel" },
|
|
4178
|
+
{ key: "tab-survives-a-reload", label: "Reloading on a tab stays on that tab rather than bouncing to Products" },
|
|
4179
|
+
{ key: "store-tabs-hide-other-stores-items", label: "No tab shows a listing belonging to a different store" },
|
|
4180
|
+
{ key: "coupons-tab-shows-this-stores-coupons", label: "The Coupons tab shows this store's coupons and not the platform-wide ones" },
|
|
4181
|
+
],
|
|
4182
|
+
},
|
|
3931
4183
|
{
|
|
3932
4184
|
pageKey: "core-listing-pages",
|
|
3933
4185
|
pageLabel: "Homepage & Core Listing Pages",
|
|
@@ -4081,6 +4333,60 @@ const rawTesterChecklistItems = [
|
|
|
4081
4333
|
href: "/how-emi-works",
|
|
4082
4334
|
},
|
|
4083
4335
|
{ key: "fees-page", label: "The fees page loads correctly", href: "/fees" },
|
|
4336
|
+
/*
|
|
4337
|
+
* The blanket case above asserts all seven how-it-works pages LOAD. These
|
|
4338
|
+
* seven assert each one is TRUE — the guide is read, then the feature it
|
|
4339
|
+
* describes is opened and compared. A guide is documentation with no
|
|
4340
|
+
* compiler behind it, so the only thing that catches drift is someone
|
|
4341
|
+
* doing both halves in one sitting; "it loads" passes against every stale
|
|
4342
|
+
* sentence on it.
|
|
4343
|
+
*/
|
|
4344
|
+
{
|
|
4345
|
+
key: "how-auctions-work-matches-product",
|
|
4346
|
+
label: "What /how-auctions-work says about bidding, increments and what happens at the end matches a real auction page",
|
|
4347
|
+
description: "Compare against auction-beyblade-original-dragoon-storm. Anti-snipe, minimum increment and reserve behaviour are the three the guide is most likely to have stale.",
|
|
4348
|
+
href: "/how-auctions-work",
|
|
4349
|
+
},
|
|
4350
|
+
{
|
|
4351
|
+
key: "how-checkout-works-matches-product",
|
|
4352
|
+
label: "What /how-checkout-works describes as the checkout steps matches the steps checkout actually presents",
|
|
4353
|
+
description: "Checkout is Address -> Add-ons & fees -> Payment. A guide still describing a two-step checkout sends a buyer looking for a screen that is not there.",
|
|
4354
|
+
href: "/how-checkout-works",
|
|
4355
|
+
},
|
|
4356
|
+
{
|
|
4357
|
+
key: "how-offers-work-matches-product",
|
|
4358
|
+
label: "What /how-offers-work says about making, countering and accepting an offer matches the offer flow on a real listing",
|
|
4359
|
+
description: "The guide must not promise the seller can counter more rounds than the product allows, and must name the same expiry behaviour the offer actually has.",
|
|
4360
|
+
href: "/how-offers-work",
|
|
4361
|
+
},
|
|
4362
|
+
{
|
|
4363
|
+
key: "how-orders-work-matches-product",
|
|
4364
|
+
label: "The order statuses named on /how-orders-work are the statuses an order can actually hold",
|
|
4365
|
+
description: "A status named in the guide that the product never sets is the tell. Compare against the status filter chips on /user/orders.",
|
|
4366
|
+
href: "/how-orders-work",
|
|
4367
|
+
},
|
|
4368
|
+
{
|
|
4369
|
+
key: "how-payouts-work-matches-product",
|
|
4370
|
+
label: "The payout schedule and deductions described on /how-payouts-work match what a seller's payouts page shows",
|
|
4371
|
+
href: "/how-payouts-work",
|
|
4372
|
+
},
|
|
4373
|
+
{
|
|
4374
|
+
key: "how-pre-orders-work-matches-product",
|
|
4375
|
+
label: "What /how-pre-orders-work says about deposits and cancellation matches a real pre-order listing",
|
|
4376
|
+
description: "Compare against preorder-beyblade-x-bx-08-wave. Deposit wording and cancellability are the two that drift.",
|
|
4377
|
+
href: "/how-pre-orders-work",
|
|
4378
|
+
},
|
|
4379
|
+
{
|
|
4380
|
+
key: "how-reviews-work-matches-product",
|
|
4381
|
+
label: "What /how-reviews-work says about who may review and when matches what the review form actually enforces",
|
|
4382
|
+
href: "/how-reviews-work",
|
|
4383
|
+
},
|
|
4384
|
+
{
|
|
4385
|
+
key: "track-order-page-works",
|
|
4386
|
+
label: "The order-tracking page at /track is reachable from the footer and does something useful for both a real order id and a nonsense one",
|
|
4387
|
+
description: "Linked from the footer's Support group and has no checklist coverage at all. The nonsense id is the control: a page that reports 'found' for every input is not looking anything up.",
|
|
4388
|
+
href: "/track",
|
|
4389
|
+
},
|
|
4084
4390
|
],
|
|
4085
4391
|
},
|
|
4086
4392
|
{
|
|
@@ -4221,6 +4527,45 @@ const rawTesterChecklistItems = [
|
|
|
4221
4527
|
label: "Opening the new-item-request page directly shows the form already open, and a submitted request appears in the item-requests list",
|
|
4222
4528
|
href: "/item-requests/new",
|
|
4223
4529
|
},
|
|
4530
|
+
/*
|
|
4531
|
+
* The three blanket cases above cover the four help sub-pages as a set —
|
|
4532
|
+
* they load, they are linked both ways, their content differs from the
|
|
4533
|
+
* parent's. These four address each page on its own terms and against the
|
|
4534
|
+
* feature it documents, which is the half no "does it load" case reaches.
|
|
4535
|
+
*/
|
|
4536
|
+
{
|
|
4537
|
+
key: "help-account-matches-product",
|
|
4538
|
+
label: "What /help/account says about changing a password, an email and closing an account matches what /user/settings actually offers",
|
|
4539
|
+
description: "Password change is a Firebase reset LINK, not an in-page form. A help page describing an in-page form sends the user hunting for a control that does not exist.",
|
|
4540
|
+
href: "/help/account",
|
|
4541
|
+
},
|
|
4542
|
+
{
|
|
4543
|
+
key: "help-auctions-matches-product",
|
|
4544
|
+
label: "What /help/auctions tells a buyer about bidding and losing matches what an auction page and /user/bids actually do",
|
|
4545
|
+
href: "/help/auctions",
|
|
4546
|
+
},
|
|
4547
|
+
{
|
|
4548
|
+
key: "help-orders-matches-product",
|
|
4549
|
+
label: "What /help/orders says about cancelling, returning and tracking matches the actions on a real order's page",
|
|
4550
|
+
description: "Compare against the row actions on /user/orders. An action the help page names but the order page does not offer is the finding.",
|
|
4551
|
+
href: "/help/orders",
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
key: "help-shopping-matches-product",
|
|
4555
|
+
label: "What /help/shopping says about carts, wishlists and coupons matches what those surfaces do for a signed-in buyer",
|
|
4556
|
+
href: "/help/shopping",
|
|
4557
|
+
},
|
|
4558
|
+
{
|
|
4559
|
+
key: "seller-guide-bundles-matches-product",
|
|
4560
|
+
label: "What /seller-guide/bundles promises matches what the seller bundle editor actually allows",
|
|
4561
|
+
description: "A bundle's members must all belong to one store — that is refused at save time. A guide that does not say so invites a seller to build a bundle that cannot be saved.",
|
|
4562
|
+
href: "/seller-guide/bundles",
|
|
4563
|
+
},
|
|
4564
|
+
{
|
|
4565
|
+
key: "seller-guide-prize-draws-matches-product",
|
|
4566
|
+
label: "What /seller-guide/prize-draws promises about entries, reveal and refunds matches the prize-draw a seller can actually create",
|
|
4567
|
+
href: "/seller-guide/prize-draws",
|
|
4568
|
+
},
|
|
4224
4569
|
],
|
|
4225
4570
|
},
|
|
4226
4571
|
{
|
|
@@ -4412,6 +4757,30 @@ const rawTesterChecklistItems = [
|
|
|
4412
4757
|
},
|
|
4413
4758
|
]),
|
|
4414
4759
|
...group("admin", ADMIN_TESTING_GROUP_LABEL, [
|
|
4760
|
+
{
|
|
4761
|
+
/*
|
|
4762
|
+
* Admin detail routes. 33 `[id]` directories under admin/ and almost all
|
|
4763
|
+
* of them sit at zero.
|
|
4764
|
+
*
|
|
4765
|
+
* Written as ROUND TRIPS — open, change one field, save, RELOAD, then
|
|
4766
|
+
* check the fields you did NOT touch. That shape is what catches an
|
|
4767
|
+
* editor seeded from the wrong object: a form that opens blank and saves
|
|
4768
|
+
* its own emptiness passes any case that only asks whether the page
|
|
4769
|
+
* loads (Root Cause #98).
|
|
4770
|
+
*/
|
|
4771
|
+
pageKey: "admin-detail-round-trips",
|
|
4772
|
+
pageLabel: "Admin detail pages — view and edit round-trips",
|
|
4773
|
+
href: "/admin/products",
|
|
4774
|
+
cases: [
|
|
4775
|
+
{ key: "coupon-edit-round-trip", label: "Editing one field of a coupon saves it and leaves every other field intact after a reload" },
|
|
4776
|
+
{ key: "faq-edit-preserves-answer-shape", label: "Editing an FAQ's answer keeps it rendering as rich text, not as raw markup or a blank" },
|
|
4777
|
+
{ key: "category-edit-keeps-hierarchy", label: "Renaming a category leaves its parent, its children and its product count unchanged" },
|
|
4778
|
+
{ key: "user-edit-keeps-tester-flags", label: "Saving an unrelated field on a tester account does NOT clear its tester flags" },
|
|
4779
|
+
{ key: "store-edit-keeps-verified", label: "Saving an unrelated field on a verified store does NOT un-verify it" },
|
|
4780
|
+
{ key: "order-view-matches-drawer", label: "An order's full page shows the same items, address, payment and status as its row drawer" },
|
|
4781
|
+
{ key: "admin-detail-missing-id-404s", label: "An admin detail URL for an id that does not exist shows 404, not an empty editor" },
|
|
4782
|
+
],
|
|
4783
|
+
},
|
|
4415
4784
|
{
|
|
4416
4785
|
pageKey: "catalog-listings",
|
|
4417
4786
|
pageLabel: "Catalog & Listings",
|
|
@@ -5423,6 +5792,108 @@ const rawTesterChecklistItems = [
|
|
|
5423
5792
|
},
|
|
5424
5793
|
],
|
|
5425
5794
|
},
|
|
5795
|
+
{
|
|
5796
|
+
/*
|
|
5797
|
+
* Step H. 55 Firebase functions — 28 scheduled, 20 Firestore triggers,
|
|
5798
|
+
* 7 HTTPS — and no checklist coverage at all.
|
|
5799
|
+
*
|
|
5800
|
+
* Every case here tests an EFFECT, never a function. A case that says
|
|
5801
|
+
* "wait for the cron" is untestable in a session and will be answered
|
|
5802
|
+
* null forever; a case that reads the document the cron is supposed to
|
|
5803
|
+
* have written is answerable in seconds. The functions are deployed
|
|
5804
|
+
* infrastructure, so the only honest question a tester can ask is
|
|
5805
|
+
* "did the thing it is responsible for actually happen".
|
|
5806
|
+
*/
|
|
5807
|
+
pageKey: "firebase-function-effects",
|
|
5808
|
+
pageLabel: "Firebase Functions — observable effects",
|
|
5809
|
+
href: "/admin/maintenance",
|
|
5810
|
+
cases: [
|
|
5811
|
+
{
|
|
5812
|
+
key: "function-invocation-health-per-function",
|
|
5813
|
+
label: "The Firebase console's per-function invocation counts show no single function running away from the rest",
|
|
5814
|
+
description: "This is the case that would have caught Root Cause #92: onShipmentHeaderWrite self-triggered 1,017,548 times in 24h against a 2M/month free quota, while every other function sat at 0-95. The AGGREGATE number never names the culprit — read the PER-FUNCTION breakdown, sort by invocations, and compare the top entry against the rest.",
|
|
5815
|
+
href: "/admin/maintenance",
|
|
5816
|
+
},
|
|
5817
|
+
{
|
|
5818
|
+
key: "function-errors-page-has-no-producer",
|
|
5819
|
+
label: "The function-errors page is EMPTY, and that is a known gap rather than a clean bill of health",
|
|
5820
|
+
description: "BEFORE: /admin/maintenance/function-errors renders a list. AFTER: it is empty — and it will be empty no matter what breaks, because nothing in production writes a serverErrors row with source:\"function\". wrapJobHandler is the only producer and it is referenced solely by its own test. Answer NO with that reasoning: a case reading \"check errors appear here\" would pass vacuously against a surface that can never populate.",
|
|
5821
|
+
href: "/admin/maintenance/function-errors",
|
|
5822
|
+
},
|
|
5823
|
+
{
|
|
5824
|
+
key: "https-function-401-is-healthy-500-is-not",
|
|
5825
|
+
label: "Each deployed HTTPS function answers 401 unauthenticated, never 500",
|
|
5826
|
+
description: "There are 7: adminAnalytics, storeAnalytics, promotionsApi, listingProcessor, triggerEventRaffle, assignSpinPrize, gateway. A 401 means the module loaded and the auth gate ran — that is HEALTHY. A 500 means the module failed to load at cold start, which is how Root Cause #69 took every route down while the build was green.",
|
|
5827
|
+
href: "/admin/maintenance",
|
|
5828
|
+
},
|
|
5829
|
+
{
|
|
5830
|
+
key: "counters-reconcile-effect-category-counts",
|
|
5831
|
+
label: "Category product counts on /categories agree with a recount of the products actually filed under each",
|
|
5832
|
+
description: "countersReconcile runs nightly and is what keeps metrics.productCount (own) and metrics.totalProductCount (own + descendants) honest. Root Cause 102: it could not express the two separately and wrote one number to both, so 19 of 65 rows disagreed with a recount, every one low. Pick the root category and one leaf, count the listings each actually shows, and compare against the displayed count.",
|
|
5833
|
+
href: "/categories",
|
|
5834
|
+
},
|
|
5835
|
+
{
|
|
5836
|
+
key: "revenue-rollup-effect-dashboard-reads-one-doc",
|
|
5837
|
+
label: "The admin dashboard's revenue figures are present and plausible without the page scanning every order",
|
|
5838
|
+
description: "revenueRollup pre-aggregates nightly into a singleton so the dashboard is a single-doc read rather than a full-collection scan — that is the pattern protecting the 50K/day Firestore read quota. A dashboard showing zero revenue while real delivered orders exist means the rollup did not run or wrote the wrong shape.",
|
|
5839
|
+
href: "/admin",
|
|
5840
|
+
},
|
|
5841
|
+
{
|
|
5842
|
+
key: "auction-settlement-effect-winner-gets-payable-line",
|
|
5843
|
+
label: "A settled auction produces a winner who can actually pay — a locked cart line, not an unpayable order",
|
|
5844
|
+
description: "Root Cause #60: auctionSettlement used to write a document to the orders collection that was not an OrderDocument, so the win rendered nowhere and no checkout would accept it — there was no way in the product for a winner to pay. The effect to check is a LOCKED line in the winner's cart that reaches checkout via the auction lane.",
|
|
5845
|
+
href: "/user/orders",
|
|
5846
|
+
},
|
|
5847
|
+
{
|
|
5848
|
+
key: "offer-expiry-effect-lapsed-offer-frees-the-cart",
|
|
5849
|
+
label: "An offer past its deadline is no longer actionable, and its lapse does not leave a locked line blocking the buyer's cart",
|
|
5850
|
+
description: "runOfferExpiry sweeps three things in one pass — pending offers past expiresAt, ACCEPTED offers past checkoutDeadline, and unpaid auction wins. The offer lane outranks the standard lane, so a leftover locked line blocks the buyer's ENTIRE cart, not just that offer.",
|
|
5851
|
+
href: "/user/offers",
|
|
5852
|
+
},
|
|
5853
|
+
{
|
|
5854
|
+
key: "payment-window-timeout-effect-expired-proof-window",
|
|
5855
|
+
label: "A manual-payment order left past its 15-minute window is resolved by the sweep rather than sitting pending forever",
|
|
5856
|
+
description: "paymentWindowTimeout and paymentReviewAutoApprove are the two sweeps that stop a manual payment stalling indefinitely. The observable effect is the order's own status and its payment panel — check one that is past its deadline.",
|
|
5857
|
+
href: "/admin/orders",
|
|
5858
|
+
},
|
|
5859
|
+
{
|
|
5860
|
+
key: "rtdb-event-channels-are-pruned",
|
|
5861
|
+
label: "The realtime event channels are not accumulating — auth, payment and bulk event nodes are pruned",
|
|
5862
|
+
description: "cleanupRtdbEvents prunes auth_events/payment_events/bulk_events at 3/15/15 minutes and email_events at 1 hour. The email one is deliberately longest because a batch can take ten minutes between triggering a send and asserting on it, and a ping deleted mid-batch reads as \"no email was sent\".",
|
|
5863
|
+
href: "/admin/maintenance",
|
|
5864
|
+
},
|
|
5865
|
+
{
|
|
5866
|
+
key: "media-tmp-cleanup-effect-aborted-uploads-removed",
|
|
5867
|
+
label: "An aborted media upload does not leave a permanent orphan under the tmp/ prefix",
|
|
5868
|
+
description: "Uploads land in tmp/ and are moved to a permanent path by finalize; mediaTmpCleanup removes whatever never finalised. Start an upload, abandon it before finalize, and confirm nothing renders it as a real asset.",
|
|
5869
|
+
href: "/admin/media",
|
|
5870
|
+
},
|
|
5871
|
+
{
|
|
5872
|
+
key: "product-write-trigger-effect-stock-and-groups",
|
|
5873
|
+
label: "Changing a product's stock updates its listing availability and any grouped listing it belongs to, without a manual refresh elsewhere",
|
|
5874
|
+
description: "onProductWrite / onProductStockChange recompute derived state — a grouped listing's activeMemberCount and visibilityStatus, and bundle sync. The effect is visible on the group or bundle, not on the product you edited.",
|
|
5875
|
+
href: "/admin/products",
|
|
5876
|
+
},
|
|
5877
|
+
{
|
|
5878
|
+
key: "order-create-trigger-effect-staff-signal",
|
|
5879
|
+
label: "A newly created order produces a staff-visible signal rather than only a row in the orders table",
|
|
5880
|
+
description: "onOrderCreate is expected to leave something an operator can notice. Staff signal is a durable record read by the admin inbox and counted by the daily digest — one row, not a personal notification per employee.",
|
|
5881
|
+
href: "/admin/orders",
|
|
5882
|
+
},
|
|
5883
|
+
{
|
|
5884
|
+
key: "shipment-trigger-no-op-guard-holds",
|
|
5885
|
+
label: "Saving a procurement shipment twice with no change does NOT produce a second recomputation",
|
|
5886
|
+
description: "This is Root Cause #92's exact mechanism. onShipmentHeaderWrite writes back to the collection it watches; its only brake is a no-op guard, and that guard compared with JSON.stringify — which is key-order sensitive, while Firestore returns map fields alphabetically. The strings could never match, so every write re-triggered. Save an unchanged shipment and confirm totalsComputedAt does not move.",
|
|
5887
|
+
href: "/admin/shipments",
|
|
5888
|
+
},
|
|
5889
|
+
{
|
|
5890
|
+
key: "scheduled-job-count-matches-registry",
|
|
5891
|
+
label: "The number of Cloud Scheduler jobs matches the number of scheduled functions actually defined",
|
|
5892
|
+
description: "28 scheduled functions means 28 Scheduler jobs, billed per registered job. This count has drifted four separate times, so RECOUNT rather than quoting a number: a job in Scheduler with no matching function is paid-for and dead, and a function with no job never runs.",
|
|
5893
|
+
href: "/admin/maintenance",
|
|
5894
|
+
},
|
|
5895
|
+
],
|
|
5896
|
+
},
|
|
5426
5897
|
], { adminOnly: true }),
|
|
5427
5898
|
// Demo fixture pair — deliberately outside group() since these two items
|
|
5428
5899
|
// exercise the confirm-bug → reopen-as-new-version lifecycle directly
|