@mohasinac/appkit 4.39.0 → 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/reviews/repository/reviews.repository.d.ts +1 -1
- package/dist/features/reviews/repository/reviews.repository.js +2 -2
- package/dist/features/stores/api/[storeSlug]/reviews/route.js +50 -13
- 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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for buying/order-detail-actions.
|
|
3
|
+
* WHAT: 7 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 THE WHOLE POST-PURCHASE MONEY SURFACE. `/user/orders/[id]/` has five
|
|
6
|
+
* subroutes — cancel, return, track, invoice, payment — and before 2026-09-14
|
|
7
|
+
* exactly ONE case in the entire 1,188-case catalogue touched any of them.
|
|
8
|
+
*
|
|
9
|
+
* 🛑 NO SEEDED ORDER BELONGS TO THE BOT. Measured on production: 0 of 64. So a
|
|
10
|
+
* case that says "open one of your orders" while signed in as the bot has no
|
|
11
|
+
* order to open, and answers `null` forever. The positive cases below BUY first;
|
|
12
|
+
* the seeded ids belonging to other personas are used only as the ownership
|
|
13
|
+
* negatives — which is the dimension nothing tested at all, and the one where a
|
|
14
|
+
* mistake is an IDOR rather than a cosmetic bug.
|
|
15
|
+
*
|
|
16
|
+
* @tag domain:tester
|
|
17
|
+
* @tag layer:seed
|
|
18
|
+
* @tag pattern:none
|
|
19
|
+
* @tag access:server-only
|
|
20
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
21
|
+
* @tag sideEffects:none
|
|
22
|
+
*/
|
|
23
|
+
export const authored = {
|
|
24
|
+
"checklist-buying-order-detail-actions-invoice-downloads-for-owner": {
|
|
25
|
+
roles: ["buyer"],
|
|
26
|
+
startPage: "/products",
|
|
27
|
+
steps: [
|
|
28
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
29
|
+
"Open /products, add any in-stock item to the cart, and complete checkout.",
|
|
30
|
+
"Open /user/orders and find the order you just placed. Write down its id from the row.",
|
|
31
|
+
"Open the order and use its Invoice action.",
|
|
32
|
+
"Read what the browser does with the response.",
|
|
33
|
+
],
|
|
34
|
+
expectedBehaviour: "The invoice is generated on demand and streamed by a route that checks the order belongs to the caller. It is not a stored file and has no public URL.",
|
|
35
|
+
expectedUiState: "A file downloads rather than rendering in the tab, and its name contains the order id. Not a JSON blob and not an error page.",
|
|
36
|
+
expectedData: { invoiceDownloaded: true },
|
|
37
|
+
endResult: "The order is unchanged by downloading its invoice; the row still shows the same status after a reload.",
|
|
38
|
+
},
|
|
39
|
+
"checklist-buying-order-detail-actions-invoice-refuses-other-buyer": {
|
|
40
|
+
roles: ["buyer"],
|
|
41
|
+
startPage: "/user/orders",
|
|
42
|
+
steps: [
|
|
43
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
44
|
+
"Navigate directly to /api/user/orders/order-1-20260721-11joon/invoice — a seeded order belonging to a different persona.",
|
|
45
|
+
"Read the response.",
|
|
46
|
+
"Now navigate to /user/orders/order-1-20260721-11joon.",
|
|
47
|
+
"Read that page.",
|
|
48
|
+
],
|
|
49
|
+
inputs: { foreignOrderId: "order-1-20260721-11joon" },
|
|
50
|
+
expectedBehaviour: "An order id is short and guessable, so ownership is checked on every hit rather than relied upon to be secret. Answering 403 instead of 404 would confirm that an order with that id exists, which is a membership oracle over the id space.",
|
|
51
|
+
expectedUiState: "Neither the invoice nor the order page shows any of that order's contents. No item titles, no address, no amounts.",
|
|
52
|
+
expectedData: { foreignInvoiceServed: false },
|
|
53
|
+
endResult: "Nothing about the other buyer's order changes.",
|
|
54
|
+
},
|
|
55
|
+
"checklist-buying-order-detail-actions-cancel-page-refuses-delivered-order": {
|
|
56
|
+
roles: ["buyer"],
|
|
57
|
+
startPage: "/user/orders",
|
|
58
|
+
steps: [
|
|
59
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
60
|
+
"Place an order, then open /user/orders and open that order.",
|
|
61
|
+
"Use the Cancel action and read what the page offers.",
|
|
62
|
+
"Complete the cancellation.",
|
|
63
|
+
"Reload the order and read its status.",
|
|
64
|
+
"Use the Cancel action again on the now-cancelled order.",
|
|
65
|
+
],
|
|
66
|
+
expectedBehaviour: "Cancellation is only offered while it is actually possible, and a second attempt on an already-cancelled order is refused with a reason rather than silently accepted. An endpoint that accepts a second cancel writes a second set of side effects — stock restored twice is the usual shape.",
|
|
67
|
+
expectedUiState: "The first attempt offers a confirmation and a reason field. After cancelling, the status reads Cancelled and the Cancel action is gone or disabled, with an explanation.",
|
|
68
|
+
expectedData: { statusAfterCancel: "cancelled" },
|
|
69
|
+
endResult: "The order stays Cancelled after a reload, and the item's stock is back on its product page.",
|
|
70
|
+
},
|
|
71
|
+
"checklist-buying-order-detail-actions-return-request-round-trip": {
|
|
72
|
+
roles: ["buyer"],
|
|
73
|
+
startPage: "/user/orders",
|
|
74
|
+
steps: [
|
|
75
|
+
"Sign in as vivaan.kapoor@gmail.com / TempPass123! — this persona owns the seeded DELIVERED orders, and a return can only be requested on one.",
|
|
76
|
+
"Open /user/orders and open order-1-20260721-11joon, which is delivered.",
|
|
77
|
+
"Use the Return action.",
|
|
78
|
+
"Give the reason: item not as described.",
|
|
79
|
+
"Submit the request.",
|
|
80
|
+
"Reload the order.",
|
|
81
|
+
"Open /user/returns and look for it.",
|
|
82
|
+
],
|
|
83
|
+
inputs: { orderId: "order-1-20260721-11joon", reason: "item not as described" },
|
|
84
|
+
expectedBehaviour: "A return request moves the order into Return Requested and appears on the returns surface. That status is deliberately an ACTIVE one — it is the seller's next action item, not a closed state — so the order must not disappear from the active list.",
|
|
85
|
+
expectedUiState: "The order's status reads Return Requested, and the same order is listed under /user/returns.",
|
|
86
|
+
expectedData: { statusAfterReturn: "return_requested" },
|
|
87
|
+
endResult: "After a reload the status and the returns listing both still show it. The reason text is retained.",
|
|
88
|
+
},
|
|
89
|
+
"checklist-buying-order-detail-actions-track-shows-real-dates": {
|
|
90
|
+
roles: ["buyer"],
|
|
91
|
+
startPage: "/user/orders",
|
|
92
|
+
steps: [
|
|
93
|
+
"Sign in as rehan.sheikh@gmail.com / TempPass123! — this persona owns the seeded SHIPPED order.",
|
|
94
|
+
"Open /user/orders and open order-1-20260718-aevnlw.",
|
|
95
|
+
"Use the Track action.",
|
|
96
|
+
"Read every step on the timeline and note which carry a date and which do not.",
|
|
97
|
+
"Compare the dates shown against the order's own placed and shipped dates.",
|
|
98
|
+
],
|
|
99
|
+
inputs: { orderId: "order-1-20260718-aevnlw" },
|
|
100
|
+
expectedBehaviour: "Each timeline step shows the date actually recorded for it. A step with no recorded date renders an em-dash rather than a guess — an invented or estimated date is worse than a blank, because it cannot be told apart from a real one.",
|
|
101
|
+
expectedUiState: "The placed and shipped steps carry dates matching the order. Steps the order has not reached show no date rather than today's.",
|
|
102
|
+
expectedData: { fabricatedDatesShown: false },
|
|
103
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
104
|
+
},
|
|
105
|
+
"checklist-buying-order-detail-actions-order-subroutes-404-for-other-users-order": {
|
|
106
|
+
roles: ["buyer"],
|
|
107
|
+
startPage: "/user/orders",
|
|
108
|
+
steps: [
|
|
109
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!, which owns no seeded orders.",
|
|
110
|
+
"Navigate to /user/orders/order-1-20260721-11joon/track.",
|
|
111
|
+
"Navigate to /user/orders/order-1-20260721-11joon/invoice.",
|
|
112
|
+
"Navigate to /user/orders/order-1-20260721-11joon/return.",
|
|
113
|
+
"Navigate to /user/orders/order-1-20260721-11joon/cancel.",
|
|
114
|
+
"Navigate to /user/orders/order-1-20260721-11joon/payment.",
|
|
115
|
+
"Read each page in turn.",
|
|
116
|
+
],
|
|
117
|
+
inputs: { foreignOrderId: "order-1-20260721-11joon" },
|
|
118
|
+
expectedBehaviour: "Ownership is enforced on EVERY subroute, not only on the order page that links to them. A subroute that checks nothing is reachable by typing a URL, and these five carry the address, the amounts and the payment details.",
|
|
119
|
+
expectedUiState: "All five show a not-found page. None shows an item title, an address, an amount or a tracking number belonging to that order.",
|
|
120
|
+
expectedData: { subroutesLeakingData: 0 },
|
|
121
|
+
endResult: "Nothing about the other buyer's order changes.",
|
|
122
|
+
},
|
|
123
|
+
"checklist-buying-order-detail-actions-order-subroutes-redirect-guest": {
|
|
124
|
+
roles: ["guest"],
|
|
125
|
+
startPage: "/user/orders",
|
|
126
|
+
steps: [
|
|
127
|
+
"Open a private window with no session.",
|
|
128
|
+
"Navigate to /user/orders/order-1-20260721-11joon/track.",
|
|
129
|
+
"Read the page.",
|
|
130
|
+
"Navigate to /user/orders and read that page too.",
|
|
131
|
+
],
|
|
132
|
+
inputs: { foreignOrderId: "order-1-20260721-11joon" },
|
|
133
|
+
expectedBehaviour: "A signed-out visitor is sent to sign in. Being blocked IS the expected result here — the failure to look for is the page rendering its content for a moment before redirecting, which is enough to read and enough for a crawler to capture.",
|
|
134
|
+
expectedUiState: "The sign-in page, or the orders page in its signed-out state. At no point does any order content appear, not even briefly.",
|
|
135
|
+
expectedData: { orderContentVisibleToGuest: false },
|
|
136
|
+
endResult: "No session is created and nothing is changed.",
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -341,4 +341,34 @@ export const authored = {
|
|
|
341
341
|
expectedData: { maxEntries: 50, duplicateProducts: 0 },
|
|
342
342
|
endResult: "The account's history holds the merged, de-duplicated set.",
|
|
343
343
|
},
|
|
344
|
+
/* ── Added 2026-09-14 (F3d) — the SIGNED-OUT wishlist ────────────────────────
|
|
345
|
+
* 20 cases existed, 2 signed-out, and both were about the login MERGE rather
|
|
346
|
+
* than about what a guest sees BEFORE logging in.
|
|
347
|
+
*/
|
|
348
|
+
"checklist-buying-wishlist-history-wishlist-guest-heart-prompts-signin": {
|
|
349
|
+
roles: ["guest"],
|
|
350
|
+
startPage: "/products/product-beyblade-burst-valkyrie",
|
|
351
|
+
steps: [
|
|
352
|
+
"Open /products/product-beyblade-burst-valkyrie while signed out.",
|
|
353
|
+
"Note the state of the wishlist heart before touching it.",
|
|
354
|
+
"Tap the heart.",
|
|
355
|
+
"Note what appears, and note the heart's state immediately afterwards.",
|
|
356
|
+
"Dismiss whatever appeared and look at the heart once more.",
|
|
357
|
+
],
|
|
358
|
+
expectedBehaviour: "BEFORE: the heart is empty. Tapping it asks the visitor to sign in. AFTER dismissing that prompt the heart is empty again. It must not fill optimistically before refusing — a filled heart that saved nothing is worse than a plain refusal, because the visitor believes the item is kept and does not come back for it.",
|
|
359
|
+
expectedUiState: "A sign-in prompt or modal appears naming what signing in will do. The heart is unfilled both before the tap and after the prompt is dismissed.",
|
|
360
|
+
endResult: "Nothing is saved. Nothing persists.",
|
|
361
|
+
},
|
|
362
|
+
"checklist-buying-wishlist-history-wishlist-guest-page-signed-out": {
|
|
363
|
+
roles: ["guest"],
|
|
364
|
+
startPage: "/wishlist",
|
|
365
|
+
steps: [
|
|
366
|
+
"Open /wishlist directly by URL while signed out.",
|
|
367
|
+
"Read exactly what the page says.",
|
|
368
|
+
"Note whether it offers a way to sign in from there.",
|
|
369
|
+
],
|
|
370
|
+
expectedBehaviour: "A signed-out visitor is asked to sign in. They are not shown a generic empty wishlist: an empty state reading 'nothing saved yet' is indistinguishable, to someone who did save things, from a wishlist whose contents were lost.",
|
|
371
|
+
expectedUiState: "The page states that signing in is required and offers a route to it. The words used are quoted in the answer, because the distinction between this and an ordinary empty state IS the case.",
|
|
372
|
+
endResult: "Read-only; nothing persists.",
|
|
373
|
+
},
|
|
344
374
|
};
|
package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for content-discovery/category-counts-and-rollup.
|
|
3
|
+
* WHAT: 8 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 THE RULE THESE CASES ENCODE: a category's count is its OWN items plus every
|
|
6
|
+
* descendant's, and clicking it must LIST exactly those items. With
|
|
7
|
+
* A → (B, C → D) and own counts A=5, B=5, C=8, D=2, the numbers are D=2, B=5,
|
|
8
|
+
* C=10 and **A=20** — and opening A lists all twenty.
|
|
9
|
+
*
|
|
10
|
+
* Both halves were wrong on 2026-09-14. The nightly reconciler could not express
|
|
11
|
+
* own-vs-rollup at all — `setMetrics` wrote one number to both fields — so it
|
|
12
|
+
* OVERWROTE the live trigger's correct values every night: 19 of 65 rows
|
|
13
|
+
* disagreed with a recount, every one low. And the child chips beneath a
|
|
14
|
+
* category header rendered the OWN count while the header rendered the rollup,
|
|
15
|
+
* so a parent reading 20 could sit above chips summing to 5.
|
|
16
|
+
*
|
|
17
|
+
* Nothing errored in either case. A wrong number is simply a number, which is
|
|
18
|
+
* why every case below compares TWO things that must agree rather than checking
|
|
19
|
+
* that a number is present.
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:tester
|
|
22
|
+
* @tag layer:seed
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:server-only
|
|
25
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
export const authored = {
|
|
29
|
+
"checklist-content-discovery-category-counts-and-rollup-leaf-count-matches-its-listing": {
|
|
30
|
+
roles: ["guest"],
|
|
31
|
+
startPage: "/categories",
|
|
32
|
+
steps: [
|
|
33
|
+
"Open /categories.",
|
|
34
|
+
"Open the tile for x-starters — a leaf with no child chips of its own.",
|
|
35
|
+
"Write down the count shown on its tile.",
|
|
36
|
+
"Open that category.",
|
|
37
|
+
"Count the product cards listed, using the pager total rather than counting by eye if there is more than one page.",
|
|
38
|
+
],
|
|
39
|
+
expectedBehaviour: "A count is a promise about a listing. On a leaf there are no descendants, so the number and the listing are the same set and any difference is a stale counter.",
|
|
40
|
+
expectedUiState: "The number on the tile equals the number of products the category page lists.",
|
|
41
|
+
expectedData: { tileCountEqualsListingTotal: true },
|
|
42
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
43
|
+
},
|
|
44
|
+
"checklist-content-discovery-category-counts-and-rollup-parent-count-includes-descendants": {
|
|
45
|
+
roles: ["guest"],
|
|
46
|
+
startPage: "/categories",
|
|
47
|
+
steps: [
|
|
48
|
+
"Open /categories/x-tops — a parent whose children include x-starters and x-boosters.",
|
|
49
|
+
"Write down the count on the parent's tile.",
|
|
50
|
+
"Open the parent and write down the count shown on each of its child chips.",
|
|
51
|
+
"Add the children's counts together.",
|
|
52
|
+
"Open the parent's own listing and note whether any product there is filed directly under the parent rather than a child.",
|
|
53
|
+
],
|
|
54
|
+
expectedBehaviour: "The parent's number is its own items plus every descendant's. A parent showing only its own items reports a number smaller than the branch it stands for, and a parent showing only its descendants loses whatever is filed directly under it.",
|
|
55
|
+
expectedUiState: "The parent's count is greater than or equal to the sum of its children's counts, and equals that sum plus anything filed directly under the parent.",
|
|
56
|
+
expectedData: { parentCountAtLeastSumOfChildren: true },
|
|
57
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
58
|
+
},
|
|
59
|
+
"checklist-content-discovery-category-counts-and-rollup-parent-listing-includes-descendant-items": {
|
|
60
|
+
roles: ["guest"],
|
|
61
|
+
startPage: "/categories",
|
|
62
|
+
steps: [
|
|
63
|
+
"Open /categories/x-tops.",
|
|
64
|
+
"Open one of its CHILD categories and write down the title of a product listed there.",
|
|
65
|
+
"Go back to the parent category's own page.",
|
|
66
|
+
"Search the parent's listing for that same product, paging through if necessary.",
|
|
67
|
+
],
|
|
68
|
+
expectedBehaviour: "The rule governs the LISTING, not only the number. A count that rolls up while the listing does not is worse than both being wrong together: the page then contradicts itself in a way that reads as a pagination problem.",
|
|
69
|
+
expectedUiState: "The child's product appears in the parent category's listing.",
|
|
70
|
+
expectedData: { childProductVisibleOnParent: true },
|
|
71
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
72
|
+
},
|
|
73
|
+
"checklist-content-discovery-category-counts-and-rollup-header-and-child-chips-agree": {
|
|
74
|
+
roles: ["guest"],
|
|
75
|
+
startPage: "/categories",
|
|
76
|
+
steps: [
|
|
77
|
+
"Open a parent category page that shows a row of child chips.",
|
|
78
|
+
"Write down the number in the page header.",
|
|
79
|
+
"Write down the number on each child chip.",
|
|
80
|
+
"Open one child and compare its own header number to the number its chip showed.",
|
|
81
|
+
],
|
|
82
|
+
expectedBehaviour: "Every number a visitor reads is the same kind of number — the rollup. Showing the rollup in the header and the own-count on the chips answers two different questions with identically-styled figures side by side, and nothing on the screen says which is which.",
|
|
83
|
+
expectedUiState: "The number on a child's chip equals the number in that child's own page header.",
|
|
84
|
+
expectedData: { chipMatchesChildHeader: true },
|
|
85
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
86
|
+
},
|
|
87
|
+
"checklist-content-discovery-category-counts-and-rollup-root-count-is-the-whole-subtree": {
|
|
88
|
+
roles: ["guest"],
|
|
89
|
+
startPage: "/categories",
|
|
90
|
+
steps: [
|
|
91
|
+
"Open /categories/spinning-tops — a root category.",
|
|
92
|
+
"Write down its count.",
|
|
93
|
+
"Open it and write down the count on each of its direct children.",
|
|
94
|
+
"Open each child in turn and write down its children's counts, until you reach leaves.",
|
|
95
|
+
"Add up every leaf's count, plus anything filed directly under an intermediate node.",
|
|
96
|
+
],
|
|
97
|
+
expectedBehaviour: "A root's count is the sum over its whole subtree. This is the case that catches a reconciler summing only ONE level of descendants — it produces a number that is plausible, consistently too low, and never flagged.",
|
|
98
|
+
expectedUiState: "The root's count equals the total counted across the subtree.",
|
|
99
|
+
expectedData: { rootCountEqualsSubtreeSum: true },
|
|
100
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
101
|
+
},
|
|
102
|
+
"checklist-content-discovery-category-counts-and-rollup-sibling-isolation": {
|
|
103
|
+
roles: ["guest"],
|
|
104
|
+
startPage: "/categories",
|
|
105
|
+
steps: [
|
|
106
|
+
"Open /categories/x-tops, which has several children.",
|
|
107
|
+
"Open the first child and write down a product title from its listing.",
|
|
108
|
+
"Open the second child's listing.",
|
|
109
|
+
"Search the second child for that product.",
|
|
110
|
+
"Compare the two children's counts.",
|
|
111
|
+
],
|
|
112
|
+
expectedBehaviour: "Roll-up goes upward only. A product must not be counted in, or listed under, a sibling branch — the failure that produces is a set of counts that all look slightly too large and still sum plausibly.",
|
|
113
|
+
expectedUiState: "The first child's product is absent from the second child's listing.",
|
|
114
|
+
expectedData: { productLeakedToSibling: false },
|
|
115
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
116
|
+
},
|
|
117
|
+
"checklist-content-discovery-category-counts-and-rollup-guest-sees-the-same-counts": {
|
|
118
|
+
roles: ["guest", "buyer"],
|
|
119
|
+
startPage: "/categories",
|
|
120
|
+
steps: [
|
|
121
|
+
"Open /categories in a private window with no session.",
|
|
122
|
+
"Write down the counts on the first four category tiles.",
|
|
123
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
124
|
+
"Open /categories again and read the same four tiles.",
|
|
125
|
+
],
|
|
126
|
+
expectedBehaviour: "A category count is a fact about the catalogue, not about the viewer. If the two differ, something viewer-scoped is leaking into a shared number — the likeliest candidate being a filter that hides test fixtures from one audience but is counted for the other.",
|
|
127
|
+
expectedUiState: "All four numbers are identical in both sessions.",
|
|
128
|
+
expectedData: { countsDifferBetweenSessions: false },
|
|
129
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
130
|
+
},
|
|
131
|
+
"checklist-content-discovery-category-counts-and-rollup-brand-count-matches-its-listing": {
|
|
132
|
+
roles: ["guest"],
|
|
133
|
+
startPage: "/brands",
|
|
134
|
+
steps: [
|
|
135
|
+
"Open /brands.",
|
|
136
|
+
"Write down the product count shown for one brand.",
|
|
137
|
+
"Open that brand's page.",
|
|
138
|
+
"Read the total the listing reports.",
|
|
139
|
+
],
|
|
140
|
+
expectedBehaviour: "A brand is a row in the same collection as a category and its count is written by the same trigger — but brands hang off `brandSlug` rather than the category chain, so they are a separate pass and can be missed entirely while every category is correct. Brand rows had no counter writer at all until 2026-09-14.",
|
|
141
|
+
expectedUiState: "The number on the brand tile equals the number of products the brand page lists.",
|
|
142
|
+
expectedData: { brandTileMatchesListing: true },
|
|
143
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for content-discovery/event-detail-subroutes.
|
|
3
|
+
* WHAT: 8 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 ALL 29 EXISTING EVENT CASES STOP AT THE INDEX. `/events/[id]/` has four
|
|
6
|
+
* subroutes — participate, leaderboard, winner, spin-results — and none of them
|
|
7
|
+
* had a single case. Several real defects were found on exactly these pages by
|
|
8
|
+
* hand during the 2026-09 run, which is the ordinary consequence of a surface
|
|
9
|
+
* nobody has a procedure for.
|
|
10
|
+
*
|
|
11
|
+
* Fixtures are named by id because event behaviour is type- and status-specific:
|
|
12
|
+
* a poll and a raffle share a route and share almost nothing else, and "open an
|
|
13
|
+
* event" is a different test every run.
|
|
14
|
+
*
|
|
15
|
+
* @tag domain:tester
|
|
16
|
+
* @tag layer:seed
|
|
17
|
+
* @tag pattern:none
|
|
18
|
+
* @tag access:server-only
|
|
19
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
20
|
+
* @tag sideEffects:none
|
|
21
|
+
*/
|
|
22
|
+
export const authored = {
|
|
23
|
+
"checklist-content-discovery-event-detail-subroutes-detail-tabs-match-event-type": {
|
|
24
|
+
roles: ["guest"],
|
|
25
|
+
startPage: "/events",
|
|
26
|
+
steps: [
|
|
27
|
+
"Open /events/event-favourite-blader-poll — a poll.",
|
|
28
|
+
"Write down every tab shown.",
|
|
29
|
+
"Open /events/event-original-series-clearance — a sale.",
|
|
30
|
+
"Write down its tabs.",
|
|
31
|
+
"Open /events/event-win-burst-regalia-genesis — a raffle.",
|
|
32
|
+
"Write down its tabs.",
|
|
33
|
+
],
|
|
34
|
+
inputs: {
|
|
35
|
+
pollEvent: "event-favourite-blader-poll",
|
|
36
|
+
saleEvent: "event-original-series-clearance",
|
|
37
|
+
raffleEvent: "event-win-burst-regalia-genesis",
|
|
38
|
+
},
|
|
39
|
+
expectedBehaviour: "The tab bar is derived from the event's type. A tab offered for a type that cannot have it leads to a page with nothing to render — and a blank panel reads as a loading failure rather than as a tab that should not exist.",
|
|
40
|
+
expectedUiState: "The poll shows no Spin Results and no Winner tab. The sale shows no Leaderboard. The raffle shows a Winner tab.",
|
|
41
|
+
expectedData: { tabWithNoContentShown: false },
|
|
42
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
43
|
+
},
|
|
44
|
+
"checklist-content-discovery-event-detail-subroutes-participate-records-an-entry": {
|
|
45
|
+
roles: ["buyer"],
|
|
46
|
+
startPage: "/events",
|
|
47
|
+
steps: [
|
|
48
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
49
|
+
"Open /events/event-favourite-blader-poll.",
|
|
50
|
+
"Open its Participate tab.",
|
|
51
|
+
"Submit the first available option.",
|
|
52
|
+
"Reload the page.",
|
|
53
|
+
"Read what the Participate tab shows now.",
|
|
54
|
+
],
|
|
55
|
+
inputs: { eventId: "event-favourite-blader-poll" },
|
|
56
|
+
expectedBehaviour: "Participation writes an entry and the page reflects it afterwards. A write that succeeds while the screen keeps offering the form is the classic split between behaviour and UI state — and it leads a user to submit repeatedly.",
|
|
57
|
+
expectedUiState: "After the reload the tab shows the submitted answer or a 'you have taken part' state, not the blank form again.",
|
|
58
|
+
expectedData: { entryPersisted: true },
|
|
59
|
+
endResult: "The entry is still recorded after a second reload.",
|
|
60
|
+
},
|
|
61
|
+
"checklist-content-discovery-event-detail-subroutes-participate-twice-is-refused": {
|
|
62
|
+
roles: ["buyer"],
|
|
63
|
+
startPage: "/events",
|
|
64
|
+
steps: [
|
|
65
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123! and open /events/event-favourite-blader-poll.",
|
|
66
|
+
"Participate once if you have not already.",
|
|
67
|
+
"Reload, and try to participate a second time with a different option.",
|
|
68
|
+
"Read what happens.",
|
|
69
|
+
"Open the Leaderboard tab and read the total entry count.",
|
|
70
|
+
],
|
|
71
|
+
inputs: { eventId: "event-favourite-blader-poll" },
|
|
72
|
+
expectedBehaviour: "A second entry from the same account is refused with a reason. Silently accepting it doubles that account's weight in the result, which is invisible until somebody reconciles the entry count against the participant count.",
|
|
73
|
+
expectedUiState: "A clear refusal naming the reason. The entry total does not rise by a second entry from this account.",
|
|
74
|
+
expectedData: { duplicateEntryAccepted: false },
|
|
75
|
+
endResult: "Exactly one entry for this account survives a reload.",
|
|
76
|
+
},
|
|
77
|
+
"checklist-content-discovery-event-detail-subroutes-leaderboard-ranks-by-a-real-number": {
|
|
78
|
+
roles: ["guest"],
|
|
79
|
+
startPage: "/events",
|
|
80
|
+
steps: [
|
|
81
|
+
"Open /events/event-tester-sandbox-top-scorers and open its Leaderboard tab.",
|
|
82
|
+
"Read the score beside each of the first five rows.",
|
|
83
|
+
"Check that the scores are in descending order.",
|
|
84
|
+
"Read the total entry count the event reports.",
|
|
85
|
+
"Count the rows the leaderboard lists, paging if needed.",
|
|
86
|
+
],
|
|
87
|
+
inputs: { eventId: "event-tester-sandbox-top-scorers" },
|
|
88
|
+
expectedBehaviour: "The leaderboard orders by a stored score and covers the same entries the event counts. A ranking that is merely insertion order looks correct whenever the data happens to arrive sorted, so the descending check has to be explicit.",
|
|
89
|
+
expectedUiState: "Scores descend down the list, and the number of entries listed matches the count the event reports.",
|
|
90
|
+
expectedData: { scoresDescending: true },
|
|
91
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
92
|
+
},
|
|
93
|
+
"checklist-content-discovery-event-detail-subroutes-winner-page-before-draw": {
|
|
94
|
+
roles: ["guest"],
|
|
95
|
+
startPage: "/events",
|
|
96
|
+
steps: [
|
|
97
|
+
"Open /events/event-win-burst-regalia-genesis — an ACTIVE raffle that has not been drawn.",
|
|
98
|
+
"Open its Winner tab.",
|
|
99
|
+
"Read the page.",
|
|
100
|
+
],
|
|
101
|
+
inputs: { eventId: "event-win-burst-regalia-genesis" },
|
|
102
|
+
expectedBehaviour: "An undrawn raffle has no winner, and the page must say so. A blank panel is indistinguishable from a failed load, and any name shown here before a draw is fabricated — which on a prize event is the worst possible thing to render.",
|
|
103
|
+
expectedUiState: "A clear statement that the draw has not happened yet, ideally with when it will. No name, no masked name, no empty winner card.",
|
|
104
|
+
expectedData: { winnerNameShown: false },
|
|
105
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
106
|
+
},
|
|
107
|
+
"checklist-content-discovery-event-detail-subroutes-winner-page-after-draw": {
|
|
108
|
+
roles: ["guest"],
|
|
109
|
+
startPage: "/events",
|
|
110
|
+
steps: [
|
|
111
|
+
"Open /events/event-won-original-set-raffle — an ENDED raffle with a recorded winner.",
|
|
112
|
+
"Open its Winner tab.",
|
|
113
|
+
"Read the winner shown and the prize named.",
|
|
114
|
+
"Open the event's Overview and compare the prize named there.",
|
|
115
|
+
],
|
|
116
|
+
inputs: { eventId: "event-won-original-set-raffle" },
|
|
117
|
+
expectedBehaviour: "The winner page reads the winner recorded on the event. The displayed name is masked, as every other public surface masks a user's name.",
|
|
118
|
+
expectedUiState: "A winner is named in masked form and the prize matches what the Overview describes.",
|
|
119
|
+
expectedData: { winnerShown: true },
|
|
120
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
121
|
+
},
|
|
122
|
+
"checklist-content-discovery-event-detail-subroutes-cancelled-event-refuses-participation": {
|
|
123
|
+
roles: ["buyer"],
|
|
124
|
+
startPage: "/events",
|
|
125
|
+
steps: [
|
|
126
|
+
"Sign in as the bot buyer claude-tester@letitrip.in / TempPass123!.",
|
|
127
|
+
"Open /events/event-x-launch-raffle-cancelled — a CANCELLED raffle.",
|
|
128
|
+
"Read the event header for its status.",
|
|
129
|
+
"Open the Participate tab.",
|
|
130
|
+
"Try to take part.",
|
|
131
|
+
],
|
|
132
|
+
inputs: { eventId: "event-x-launch-raffle-cancelled" },
|
|
133
|
+
expectedBehaviour: "A cancelled event accepts nothing. The status is checked server-side as well as hidden in the UI: a Participate tab that merely hides its button still has an endpoint behind it.",
|
|
134
|
+
expectedUiState: "The header says Cancelled, and the Participate tab explains that the event is closed rather than offering a form.",
|
|
135
|
+
expectedData: { participationAccepted: false },
|
|
136
|
+
endResult: "No entry is recorded; the entry count is unchanged after a reload.",
|
|
137
|
+
},
|
|
138
|
+
"checklist-content-discovery-event-detail-subroutes-guest-sees-event-but-is-prompted": {
|
|
139
|
+
roles: ["guest"],
|
|
140
|
+
startPage: "/events",
|
|
141
|
+
steps: [
|
|
142
|
+
"Open a private window with no session.",
|
|
143
|
+
"Open /events/event-favourite-blader-poll.",
|
|
144
|
+
"Read the Overview — the title, the description and the dates.",
|
|
145
|
+
"Open the Participate tab.",
|
|
146
|
+
"Read what it offers.",
|
|
147
|
+
],
|
|
148
|
+
inputs: { eventId: "event-favourite-blader-poll" },
|
|
149
|
+
expectedBehaviour: "An event is public reading and gated taking-part. Being prompted to sign in IS the expected result — the failure to look for is a hard redirect off the page, which loses the visitor the content they came for, or a form that submits and fails.",
|
|
150
|
+
expectedUiState: "The event's own content is readable. Participate shows a sign-in prompt rather than a form, and nothing 404s or bounces to /unauthorized.",
|
|
151
|
+
expectedData: { eventContentReadableByGuest: true },
|
|
152
|
+
endResult: "No entry is recorded and no session is created.",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for content-discovery/store-reviews-aggregate.
|
|
3
|
+
* WHAT: 4 cases, keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 THIS TAB ANSWERED 200 WITH ZERO REVIEWS FOR EVERY STORE, for as long as it
|
|
6
|
+
* had existed, while 79 approved reviews sat in Firestore (Root Cause #100). It
|
|
7
|
+
* selected "the store's top 20 products by `itemsSold`" and fanned out a review
|
|
8
|
+
* query per product — and **no product carries `itemsSold`**, so Firestore's
|
|
9
|
+
* `orderBy` excluded all 95 of them and the endpoint returned nothing.
|
|
10
|
+
*
|
|
11
|
+
* The only symptom was "this store has no reviews yet", which reads as a fact
|
|
12
|
+
* about the store. That is why the first case names a store KNOWN to have
|
|
13
|
+
* reviews rather than asking whether the tab renders.
|
|
14
|
+
*
|
|
15
|
+
* Fixing it then exposed the next layer: the route returned the raw document,
|
|
16
|
+
* including the reviewer's name as `enc:v1:…` ciphertext and the HMAC blind
|
|
17
|
+
* index beside it. Hence the privacy case.
|
|
18
|
+
*
|
|
19
|
+
* @tag domain:tester
|
|
20
|
+
* @tag layer:seed
|
|
21
|
+
* @tag pattern:none
|
|
22
|
+
* @tag access:server-only
|
|
23
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
24
|
+
* @tag sideEffects:none
|
|
25
|
+
*/
|
|
26
|
+
export const authored = {
|
|
27
|
+
"checklist-content-discovery-store-reviews-aggregate-reviews-tab-not-empty": {
|
|
28
|
+
roles: ["guest"],
|
|
29
|
+
startPage: "/stores/store-beyblade-arena",
|
|
30
|
+
steps: [
|
|
31
|
+
"Open /stores/store-beyblade-arena — this store is known to have many approved reviews, which is the point of naming it.",
|
|
32
|
+
"Open its Reviews tab.",
|
|
33
|
+
"Wait for the content to finish loading before judging it.",
|
|
34
|
+
"Read how many reviews are reported and count the cards on the first page.",
|
|
35
|
+
],
|
|
36
|
+
expectedBehaviour: "Reviews are fetched by the store they belong to. Routing the lookup through a sample of the store's products makes the result depend on a field those products may not have — and an ordering field that is absent silently excludes every document, returning an empty list rather than an error.",
|
|
37
|
+
expectedUiState: "Review cards are listed and a total is shown. NOT 'No reviews yet' and not an empty tab.",
|
|
38
|
+
expectedData: { totalReviewsAtLeast: 1 },
|
|
39
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
40
|
+
},
|
|
41
|
+
"checklist-content-discovery-store-reviews-aggregate-average-matches-the-rows": {
|
|
42
|
+
roles: ["guest"],
|
|
43
|
+
startPage: "/stores/store-beyblade-arena",
|
|
44
|
+
steps: [
|
|
45
|
+
"Open the store's Reviews tab and write down the average rating and the total.",
|
|
46
|
+
"Read the star-distribution breakdown and write down the count beside each star level.",
|
|
47
|
+
"Add the five distribution counts together.",
|
|
48
|
+
"Compare that sum with the total.",
|
|
49
|
+
"Filter to 5-star reviews and compare the number of cards with the 5-star count from the breakdown.",
|
|
50
|
+
],
|
|
51
|
+
expectedBehaviour: "The aggregate covers the whole store, not a sample of it. Computing it from an arbitrary subset of products leaves the average and the distribution quietly wrong — and those two numbers are what a buyer judges a seller on.",
|
|
52
|
+
expectedUiState: "The distribution counts sum to the total, and the 5-star filter returns as many reviews as the breakdown claims.",
|
|
53
|
+
expectedData: { distributionSumsToTotal: true },
|
|
54
|
+
endResult: "Clearing the filter restores the full list.",
|
|
55
|
+
},
|
|
56
|
+
"checklist-content-discovery-store-reviews-aggregate-reviewer-name-is-masked": {
|
|
57
|
+
roles: ["guest"],
|
|
58
|
+
startPage: "/stores/store-beyblade-arena",
|
|
59
|
+
steps: [
|
|
60
|
+
"Open the store's Reviews tab.",
|
|
61
|
+
"Read the reviewer name shown on the first three cards.",
|
|
62
|
+
"Open the browser network panel and reload.",
|
|
63
|
+
"Find the request that fetched the reviews and read its response body.",
|
|
64
|
+
"Search that body for the text enc:v1 and for the field names userNameIndex, searchTxt and userId.",
|
|
65
|
+
],
|
|
66
|
+
expectedBehaviour: "A reviewer's name is PII, stored encrypted, and shown masked in public. The generic repository returns the document verbatim, so a route that does not go through the reviews repository serves ciphertext — and a payload built by spreading the document also carries the HMAC blind index, which lets a caller confirm a guessed name.",
|
|
67
|
+
expectedUiState: "Names render masked, for example 'M*** U***'. No card shows a value beginning enc:v1.",
|
|
68
|
+
expectedData: { ciphertextInResponse: false, blindIndexInResponse: false },
|
|
69
|
+
endResult: "Nothing is changed; this case only reads.",
|
|
70
|
+
},
|
|
71
|
+
"checklist-content-discovery-store-reviews-aggregate-review-search-matches-body-text": {
|
|
72
|
+
roles: ["guest"],
|
|
73
|
+
startPage: "/stores/store-beyblade-arena",
|
|
74
|
+
steps: [
|
|
75
|
+
"Open the store's Reviews tab and read one review card in full.",
|
|
76
|
+
"Pick a distinctive word that appears in that review's BODY text but not in its title and not in the product name.",
|
|
77
|
+
"Type that word into the reviews search box.",
|
|
78
|
+
"Read the results.",
|
|
79
|
+
"Clear the box and search for zzzznope.",
|
|
80
|
+
"Read the results again.",
|
|
81
|
+
],
|
|
82
|
+
inputs: { nonsenseQuery: "zzzznope" },
|
|
83
|
+
expectedBehaviour: "Search covers the review's body. The stored field for that text is `comment`; a filter reading a differently-named field matches nothing in the body and still returns rows via the title and the product name, so it looks like a working search that is merely strict.",
|
|
84
|
+
expectedUiState: "The body word returns at least the review it came from. 'zzzznope' returns an empty state rather than the unfiltered list.",
|
|
85
|
+
expectedData: { nonsenseResultCount: 0 },
|
|
86
|
+
endResult: "Clearing the box restores the full list.",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -130,6 +130,17 @@ import { authored as a_search_and_nav__employee_permissions } from "./search-and
|
|
|
130
130
|
import { authored as a_search_and_nav__header_search } from "./search-and-nav__header-search";
|
|
131
131
|
import { authored as a_search_and_nav__settings_deep_links } from "./search-and-nav__settings-deep-links";
|
|
132
132
|
import { authored as a_search_and_nav__sidebar_search } from "./search-and-nav__sidebar-search";
|
|
133
|
+
import { authored as a_selling__listing_edit_roundtrip } from "./selling__listing-edit-roundtrip";
|
|
134
|
+
import { authored as a_selling__digital_content_delivery } from "./selling__digital-content-delivery";
|
|
135
|
+
import { authored as a_selling__seller_orders_at_scale } from "./selling__seller-orders-at-scale";
|
|
136
|
+
import { authored as a_content_discovery__category_counts_and_rollup } from "./content-discovery__category-counts-and-rollup";
|
|
137
|
+
import { authored as a_content_discovery__store_reviews_aggregate } from "./content-discovery__store-reviews-aggregate";
|
|
138
|
+
import { authored as a_buying__order_detail_actions } from "./buying__order-detail-actions";
|
|
139
|
+
import { authored as a_admin__admin_detail_round_trips } from "./admin__admin-detail-round-trips";
|
|
140
|
+
import { authored as a_content_discovery__event_detail_subroutes } from "./content-discovery__event-detail-subroutes";
|
|
141
|
+
import { authored as a_public_pages__newly_wired_browse_indexes } from "./public-pages__newly-wired-browse-indexes";
|
|
142
|
+
import { authored as a_public_pages__store_tabs } from "./public-pages__store-tabs";
|
|
143
|
+
import { authored as a_admin__firebase_function_effects } from "./admin__firebase-function-effects";
|
|
133
144
|
export const AUTHORED_CASES = {
|
|
134
145
|
...a_admin__blog_faqs,
|
|
135
146
|
...a_admin__bundles,
|
|
@@ -240,4 +251,15 @@ export const AUTHORED_CASES = {
|
|
|
240
251
|
...a_search_and_nav__header_search,
|
|
241
252
|
...a_search_and_nav__settings_deep_links,
|
|
242
253
|
...a_search_and_nav__sidebar_search,
|
|
254
|
+
...a_selling__listing_edit_roundtrip,
|
|
255
|
+
...a_selling__digital_content_delivery,
|
|
256
|
+
...a_selling__seller_orders_at_scale,
|
|
257
|
+
...a_content_discovery__category_counts_and_rollup,
|
|
258
|
+
...a_content_discovery__store_reviews_aggregate,
|
|
259
|
+
...a_buying__order_detail_actions,
|
|
260
|
+
...a_admin__admin_detail_round_trips,
|
|
261
|
+
...a_content_discovery__event_detail_subroutes,
|
|
262
|
+
...a_public_pages__newly_wired_browse_indexes,
|
|
263
|
+
...a_public_pages__store_tabs,
|
|
264
|
+
...a_admin__firebase_function_effects,
|
|
243
265
|
};
|