@mohasinac/appkit 4.35.4 → 4.37.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.
Files changed (31) hide show
  1. package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
  2. package/dist/_internal/server/features/site-settings/adapters.js +25 -1
  3. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +73 -6
  4. package/dist/_internal/shared/features/site-settings/types.d.ts +12 -0
  5. package/dist/features/tester/schemas/firestore.d.ts +29 -0
  6. package/dist/features/tester/seed-data/_money-flows.js +13 -5
  7. package/dist/features/tester/seed-data/authored/_types.d.ts +24 -0
  8. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +2 -0
  9. package/dist/features/tester/seed-data/authored/account-auth__signup-login.js +22 -0
  10. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.d.ts +2 -0
  11. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +106 -0
  12. package/dist/features/tester/seed-data/authored/admin__site-system.js +2 -0
  13. package/dist/features/tester/seed-data/authored/buying__my-orders.js +2 -0
  14. package/dist/features/tester/seed-data/authored/buying__offers.js +2 -0
  15. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +2 -0
  16. package/dist/features/tester/seed-data/authored/content-discovery__search.js +48 -19
  17. package/dist/features/tester/seed-data/authored/index.js +6 -0
  18. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +4 -0
  19. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +0 -16
  20. package/dist/features/tester/seed-data/authored/selling__product-upload-details.d.ts +2 -0
  21. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +150 -0
  22. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.d.ts +2 -0
  23. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +154 -0
  24. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +2 -0
  25. package/dist/features/tester/seed-data/index.d.ts +1 -0
  26. package/dist/features/tester/seed-data/index.js +1 -0
  27. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +111 -6
  28. package/dist/features/tester/seed-data/tester-responses-seed-data.d.ts +2 -0
  29. package/dist/features/tester/seed-data/tester-responses-seed-data.js +95 -0
  30. package/package.json +1 -1
  31. package/scripts/seed-cli.mjs +797 -752
@@ -39,17 +39,30 @@ export const authored = {
39
39
  roles: ["guest"],
40
40
  startPage: "/",
41
41
  steps: [
42
+ // The batch seeds TWO probe rows: product-{{w}}-draft-probe (status
43
+ // "draft") and product-{{w}}-archived-probe (status "archived"), both
44
+ // titled so the query below matches them. Without a KNOWN unpublished row
45
+ // the case cannot distinguish "drafts are correctly hidden" from "there
46
+ // were no drafts" — a tester reported exactly that: the suggestion payload
47
+ // carries no status field to inspect, and no draft existed to probe with.
42
48
  "Open / in a private window with no session.",
43
- "Click into the header search and type beyblade.",
49
+ "Click into the header search and type draftprobe.",
44
50
  "Read every suggestion and note its title.",
51
+ "Check specifically whether product-{{w}}-draft-probe or product-{{w}}-archived-probe appear.",
52
+ "Now type beyblade and read every suggestion.",
45
53
  "Open each suggestion in turn and read the page it lands on.",
46
54
  "Note any that 404s or shows an unpublished listing.",
47
55
  ],
48
- inputs: { query: "beyblade" },
56
+ inputs: {
57
+ query: "beyblade",
58
+ probeQuery: "draftprobe",
59
+ draftProduct: "product-{{w}}-draft-probe",
60
+ archivedProduct: "product-{{w}}-archived-probe",
61
+ },
49
62
  expectedBehaviour: "Suggestions are filtered to published, publicly-visible listings. A status filter applied only when the caller explicitly asks for one is absent here, because a typeahead rarely asks — which is exactly how draft rows reach a public suggestion list.",
50
- expectedUiState: "Every suggestion opens a real published listing. None 404s, none opens a draft or archived listing, and none names a tester sandbox item for a signed-out visitor.",
51
- expectedData: { draftSuggestions: 0 },
52
- endResult: "Read-only; nothing persists.",
63
+ expectedUiState: "Searching 'draftprobe' returns NEITHER probe row — that is the real test, because both exist and both match the term. Then every 'beyblade' suggestion opens a real published listing: none 404s, none opens a draft or archived listing, and none names a tester sandbox item for a signed-out visitor.",
64
+ expectedData: { draftSuggestions: 0, probeRowsSuggested: 0 },
65
+ endResult: "Read-only; both probe rows are removed when the batch ends.",
53
66
  },
54
67
  "checklist-content-discovery-search-search-prefix-match": {
55
68
  roles: ["guest"],
@@ -101,16 +114,24 @@ export const authored = {
101
114
  roles: ["guest"],
102
115
  startPage: "/products",
103
116
  steps: [
104
- "Open /products in a private window.",
117
+ // PRECONDITION FIRST. Without it a broken sort reads as "inconclusive"
118
+ // instead of "fail" — which is exactly what happened: a tester reported
119
+ // sorts=price and sorts=-price returning an IDENTICAL order with no
120
+ // search term at all, and had no way to record that as a verdict.
121
+ "Open /products in a private window with NO search term.",
122
+ "Set the sort to 'Price: low to high' and read the first three prices in order.",
123
+ "Set the sort to 'Price: high to low' and read the first three prices again.",
124
+ "If those two orders are identical, STOP and answer NO — sort is broken before search is involved, and say so. Do not continue.",
105
125
  "Type beyblade in the search field and click 'Search'.",
106
126
  "Read the card order and note the sort dropdown's selection.",
107
- "Change the sort to price, low to high.",
127
+ "Change the sort to 'Price: low to high'.",
108
128
  "Read the card order, the URL and the search field.",
109
129
  "Reload the page and read all three again.",
110
130
  ],
111
- inputs: { query: "beyblade", sort: "Price: low to high" },
131
+ inputs: { query: "beyblade", sort: "Price: low to high", sortReverse: "Price: high to low" },
112
132
  expectedBehaviour: "Changing the sort re-orders the current results and keeps the term. Both live in the URL, so a reload reproduces the same page — and the sort field must be one the query can actually sort on, since an unsortable field is dropped and the dropdown silently reorders nothing.",
113
- expectedUiState: "Prices ascend down the page and the search field still holds 'beyblade'. The URL carries both. After the reload the order and the term are unchanged. A dropdown selection that changes no order at all is the dropped-sort failure.",
133
+ expectedUiState: "With no search term, ascending and descending produce DIFFERENT orders — that is the precondition. Then, with the term applied, prices ascend down the page and the search field still holds 'beyblade'. The URL carries both. After the reload the order and the term are unchanged. A dropdown selection that changes no order at all is the dropped-sort failure, and it is a NO rather than an unanswerable.",
134
+ expectedData: { ascendingDiffersFromDescending: true },
114
135
  endResult: "Read-only; nothing persists beyond the URL.",
115
136
  },
116
137
  "checklist-content-discovery-search-search-keeps-facets": {
@@ -126,8 +147,8 @@ export const authored = {
126
147
  ],
127
148
  inputs: { query: "beyblade", priceMin: 500, priceMax: 1500 },
128
149
  expectedBehaviour: "Price, tag and availability all narrow alongside the term. A facet that renders and counts toward the badge while changing nothing is inert — that happens when it emits a field the query allowlist does not carry, and nothing raises.",
129
- expectedUiState: "Each facet visibly reduces the count and every card respects it. The availability tab changes which rows appear. A facet that leaves the count identical while the filter badge increments is the failure.",
130
- expectedData: { inertFacetCount: 0 },
150
+ expectedUiState: "Each facet visibly reduces the count and every card respects it. The availability tab changes which rows appear. A facet that leaves the count identical while the filter badge increments is the failure. RECORD ALL THREE COUNTS SEPARATELY — price, tag, availability. This case names those three specifically; exercising a different facet (category, say) and reporting a partial pass answers a question nobody asked. If one of the three cannot be reached, answer NO or null for that reason rather than passing on the strength of the other two.",
151
+ expectedData: { inertFacetCount: 0, facetsExercised: 3 },
131
152
  endResult: "Read-only; nothing persists beyond the URL.",
132
153
  },
133
154
  "checklist-content-discovery-search-search-case-and-accents": {
@@ -182,10 +203,11 @@ export const authored = {
182
203
  startPage: "/faqs",
183
204
  steps: [
184
205
  "Open /faqs in a private window and read the category list.",
185
- "Open the Shipping category page.",
206
+ "CLICK INTO the Shipping category — this case is about the category PAGE, so reading the counters on /faqs is not an answer to it.",
186
207
  "Read the questions listed and expand one to read its answer.",
187
208
  "Read the address bar.",
188
209
  "Open a second category page and read its questions.",
210
+ "Compare the two lists of questions against each other.",
189
211
  ],
190
212
  expectedBehaviour: "Each category has its own page, keyed on the category's slug, rendering only its questions. Two category pages showing the same questions means the slug is not reaching the query.",
191
213
  expectedUiState: "The Shipping page lists shipping questions and an expanded answer renders as formatted text, not as raw HTML shown as visible characters. The second category shows a different set.",
@@ -318,15 +340,22 @@ export const authored = {
318
340
  startPage: "/admin/products",
319
341
  steps: [
320
342
  "Sign in as admin@letitrip.in / TempPass123!.",
321
- "Open /admin/products and sort by oldest first.",
322
- "Read the title of the oldest listing.",
323
- "Search for a distinctive word from that title.",
324
- "Read whether that listing is returned.",
325
- "Repeat with the newest listing's title as a comparison.",
343
+ // The batch seeds product-{{w}}-untokenised with NO searchTokens field —
344
+ // a stand-in for a record written before the token field existed. On a
345
+ // freshly-seeded catalogue every row carries current tokens, so without
346
+ // this fixture the case is unanswerable by construction, which is exactly
347
+ // what a tester reported: "the catalogue was freshly seeded... there is no
348
+ // older record to probe with".
349
+ "Open /admin/products and search for the exact word 'untokenised'.",
350
+ "Read whether product-{{w}}-untokenised is returned.",
351
+ "Search for a distinctive word from any ordinary seeded listing's title.",
352
+ "Read whether that one is returned.",
353
+ "Compare the two results.",
326
354
  ],
355
+ inputs: { untokenisedProduct: "product-{{w}}-untokenised", query: "untokenised" },
327
356
  expectedBehaviour: "Search is backed by a normalised token field written on save. Records created before that field existed have no tokens, so they are unfindable — the fix is a backfill, and this case is what detects whether one is needed.",
328
- expectedUiState: "Both the oldest and the newest listing are returned by a word from their own titles. The newest being findable while the oldest is not is the signature of missing tokens on older rows, not of a broken search.",
357
+ expectedUiState: "BOTH listings are returned by a word from their own titles. The ordinary one being findable while product-{{w}}-untokenised is not is the signature of missing tokens on older rows, not of a broken search — and it means a backfill is owed.",
329
358
  expectedData: { oldRecordFound: true },
330
- endResult: "Read-only; nothing persists. Testing only recent records would pass this case while the backfill gap remains.",
359
+ endResult: "Read-only; the fixture is removed when the batch ends. Testing only recent records would pass this case while the backfill gap remains.",
331
360
  },
332
361
  };
@@ -34,6 +34,7 @@ import { authored as a_admin__catalog_listings } from "./admin__catalog-listings
34
34
  import { authored as a_admin__orders_fulfillment } from "./admin__orders-fulfillment";
35
35
  import { authored as a_admin__users_trust } from "./admin__users-trust";
36
36
  import { authored as a_admin__site_system } from "./admin__site-system";
37
+ import { authored as a_admin__bug_hunter_rewards } from "./admin__bug-hunter-rewards";
37
38
  import { authored as a_admin__category_brand_authoring } from "./admin__category-brand-authoring";
38
39
  import { authored as a_admin__uncovered_admin_pages } from "./admin__uncovered-admin-pages";
39
40
  import { authored as a_admin__bans_and_trust } from "./admin__bans-and-trust";
@@ -119,6 +120,8 @@ import { authored as a_selling__seller_orders } from "./selling__seller-orders";
119
120
  import { authored as a_selling__seller_marketing_extras } from "./selling__seller-marketing-extras";
120
121
  import { authored as a_selling__listing_a_product } from "./selling__listing-a-product";
121
122
  import { authored as a_selling__seller_listing_types } from "./selling__seller-listing-types";
123
+ import { authored as a_selling__product_upload_details } from "./selling__product-upload-details";
124
+ import { authored as a_selling__quick_add_minimum_details } from "./selling__quick-add-minimum-details";
122
125
  import { authored as a_seo__canonical_and_host } from "./seo__canonical-and-host";
123
126
  import { authored as a_seo__sitemap_and_robots } from "./seo__sitemap-and-robots";
124
127
  import { authored as a_seo__page_metadata } from "./seo__page-metadata";
@@ -141,6 +144,7 @@ export const AUTHORED_CASES = {
141
144
  ...a_admin__orders_fulfillment,
142
145
  ...a_admin__users_trust,
143
146
  ...a_admin__site_system,
147
+ ...a_admin__bug_hunter_rewards,
144
148
  ...a_admin__category_brand_authoring,
145
149
  ...a_admin__uncovered_admin_pages,
146
150
  ...a_admin__bans_and_trust,
@@ -226,6 +230,8 @@ export const AUTHORED_CASES = {
226
230
  ...a_selling__seller_marketing_extras,
227
231
  ...a_selling__listing_a_product,
228
232
  ...a_selling__seller_listing_types,
233
+ ...a_selling__product_upload_details,
234
+ ...a_selling__quick_add_minimum_details,
229
235
  ...a_seo__canonical_and_host,
230
236
  ...a_seo__sitemap_and_robots,
231
237
  ...a_seo__page_metadata,
@@ -36,6 +36,8 @@ export const authored = {
36
36
  endResult: "The account exists and persists. This case creates a real row that teardown does not remove, which is why the address is numbered rather than fixed.",
37
37
  },
38
38
  "checklist-public-pages-auth-error-pages-forgot-reset-password-pages": {
39
+ requiresHumanChannel: true,
40
+ humanChannelReason: "real email inbox",
39
41
  roles: ["guest"],
40
42
  startPage: "/auth/forgot-password",
41
43
  steps: [
@@ -58,6 +60,8 @@ export const authored = {
58
60
  endResult: "The password is back at TempPass123! by the final step. This case shares divya.funko@gmail.com with the single-use-link case in signup-login and with nothing else, deliberately — the seeder never resets an existing password, so a changed one outlives every future re-seed.",
59
61
  },
60
62
  "checklist-public-pages-auth-error-pages-verify-email-page": {
63
+ requiresHumanChannel: true,
64
+ humanChannelReason: "real email inbox",
61
65
  roles: ["guest"],
62
66
  startPage: "/auth/register",
63
67
  steps: [
@@ -168,22 +168,6 @@ export const authored = {
168
168
  expectedUiState: "Every preview shows its picture at the same size as its neighbours, at both widths. An empty white or bordered box where a photo should be is the failure, and it hits every clickable image tile in the app at once.",
169
169
  endResult: "Leave the editor without saving.",
170
170
  },
171
- "checklist-selling-listing-a-product-media-upload-images-capped-at-5": {
172
- roles: ["seller"],
173
- startPage: "/store/products/new",
174
- steps: [
175
- "Sign in as tyson@beybladearena.in / TempPass123!.",
176
- "Open /store/products/new and read the stated image limit before uploading anything.",
177
- "Upload public/test-media/sample-image.png repeatedly, counting the previews after each.",
178
- "Continue until an upload is refused.",
179
- "Read the message and the preview count at that point.",
180
- "Compare the count against the limit the form stated.",
181
- ],
182
- inputs: { image: "public/test-media/sample-image.png" },
183
- expectedBehaviour: "The number the form STATES is the number it enforces. This case exists to catch a disagreement between them: a label saying five against a limit of ten, or the reverse, is a promise the form does not keep — and the checklist label itself says five while the shared limits module is the authority.",
184
- expectedUiState: "Uploads are accepted up to the stated limit and the next is refused with a readable message. The refusal count equals the stated limit exactly. If the two disagree, record BOTH numbers — which one is wrong is a judgement for the reader, not for the tester.",
185
- endResult: "Leave the editor without saving.",
186
- },
187
171
  "checklist-selling-listing-a-product-media-upload-video-duration": {
188
172
  roles: ["seller"],
189
173
  startPage: "/store/products/new",
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,150 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the selling/product-upload-details page.
3
+ * WHAT: 6 case(s), keyed by full checklist id.
4
+ *
5
+ * The seller product form has THREE separate media controls and they are easy to
6
+ * conflate. Grounded in SellerProductShell.tsx `StepMedia` (:342-497):
7
+ *
8
+ * Main image ImageUpload, label "Main Image", enableAdvancedCrop,
9
+ * cropAspectRatio 1, max 10 MB, index 1
10
+ * Gallery MediaUploadList, label "Gallery (up to 10 images + 1 video)",
11
+ * maxItems 11, maxImages 10, maxVideos 1,
12
+ * maxSizeMB 50, indices 2..11
13
+ * Video panel MediaUploadField, YouTube / external URL / trim / poster
14
+ *
15
+ * The existing pages already cover the CAPS (selling/media-limits: 10th uploads,
16
+ * 11th refused, second video refused) and the basics (selling/listing-a-product:
17
+ * upload, preview, remove, crop, multiple). This page covers what neither does:
18
+ * refusal copy, type refusal, ordering, index collision, and the video path that
19
+ * was returning a 500 until 2026-09.
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
+ import { MEDIA_ENDPOINTS } from "../../../../constants/api-endpoints";
29
+ /*
30
+ * The endpoint comes from the registry, not a literal.
31
+ *
32
+ * These cases genuinely need to name it — the tester is told to open the
33
+ * network panel and read that exact request, and "the signing request" would be
34
+ * too vague for R8, which requires literal values. But a pasted copy of the path
35
+ * is a second definition that silently rots the day the route moves, leaving a
36
+ * tester hunting a request that no longer exists and reporting a bug that is
37
+ * really a stale case. Interpolating keeps it precise AND single-sourced.
38
+ */
39
+ const SIGN = MEDIA_ENDPOINTS.SIGN;
40
+ const SIGN_IN_SELLER = "Sign in as tyson@beybladearena.in / TempPass123!.";
41
+ export const authored = {
42
+ "checklist-selling-product-upload-details-main-image-crop-applies": {
43
+ roles: ["seller"],
44
+ startPage: "/store/products/new",
45
+ steps: [
46
+ SIGN_IN_SELLER,
47
+ "Open /store/products/new and click 'Show all fields (advanced)' to reach the sectioned form.",
48
+ "Open the Media section and choose public/test-media/sample-image.png for 'Main Image'.",
49
+ "Wait for the crop dialog to appear and note the shape of the crop box.",
50
+ "Drag the crop box to cover clearly only the LEFT half of the picture, then confirm.",
51
+ "Read the thumbnail that appears afterwards and compare what it shows against the original.",
52
+ ],
53
+ inputs: { image: "public/test-media/sample-image.png" },
54
+ expectedBehaviour: "The crop is applied to the bytes that are uploaded, not merely previewed. A cropper that shows a selection and then stores the original is worse than no cropper — the seller believes they framed the photo and the marketplace shows something else. The box is locked square because the main image is rendered square on every card.",
55
+ expectedUiState: "A crop dialog opens before any upload begins, and its box is square and stays square when dragged. The resulting thumbnail shows the LEFT half that was selected, not the whole picture.",
56
+ expectedData: { cropAspectRatio: 1 },
57
+ endResult: "Leave without saving. Re-open the form: no half-uploaded image is left attached to a draft.",
58
+ },
59
+ "checklist-selling-product-upload-details-oversize-image-refused-client-side": {
60
+ roles: ["seller"],
61
+ startPage: "/store/products/new",
62
+ steps: [
63
+ SIGN_IN_SELLER,
64
+ "Open /store/products/new and switch to the advanced form.",
65
+ "Open the Media section and read the size limit stated next to 'Main Image'.",
66
+ "Choose public/test-media/oversized.png for the Main Image and start a timer. (The batch generates this file — it is >10 MB of incompressible noise and deliberately not committed.)",
67
+ "Read the message that appears and note how long it took.",
68
+ `Open the browser network panel and check whether any request to ${SIGN} was made.`,
69
+ ],
70
+ inputs: { image: "public/test-media/oversized.png", limitMb: 10 },
71
+ expectedBehaviour: "A file over the limit is refused BEFORE any bytes leave the browser. The point is the seller's time: a 12 MB upload that travels, then finalises, then fails validation has taken a minute to tell them something the form already knew when they picked the file. The limit is also stated up front rather than only in the refusal.",
72
+ expectedUiState: `The refusal is immediate (well under a second) and reads 'File size must be less than 10MB', naming the actual size. No ${SIGN} request appears in the network panel. The field keeps whatever image it had before.`,
73
+ expectedData: { signRequestsMade: 0 },
74
+ endResult: "Leave without saving; nothing was uploaded.",
75
+ },
76
+ "checklist-selling-product-upload-details-disallowed-type-refused": {
77
+ roles: ["seller"],
78
+ startPage: "/store/products/new",
79
+ steps: [
80
+ SIGN_IN_SELLER,
81
+ "Open /store/products/new and switch to the advanced form.",
82
+ "Open the Media section and choose public/test-media/sample-vector.svg for the Main Image.",
83
+ "Read the message that appears.",
84
+ "Now choose public/test-media/sample-image.png for the same field.",
85
+ "Read whether it is accepted.",
86
+ ],
87
+ inputs: { rejected: "public/test-media/sample-vector.svg", accepted: "public/test-media/sample-image.png" },
88
+ expectedBehaviour: "SVG is deliberately outside the allowed image types — it is a document format that can carry script, and it is served from our own origin. The refusal happens on type, not on extension alone, and the control recovers: refusing one file must not leave the field wedged.",
89
+ expectedUiState: "The SVG is refused with a message naming the accepted types. Immediately afterwards the PNG uploads normally in the same field — no reload needed.",
90
+ endResult: "Leave without saving.",
91
+ },
92
+ "checklist-selling-product-upload-details-gallery-order-persists": {
93
+ roles: ["seller"],
94
+ startPage: "/store/products/new",
95
+ steps: [
96
+ SIGN_IN_SELLER,
97
+ "Open /store/products/new and switch to the advanced form.",
98
+ "Open the Media section and add three gallery images one at a time: public/test-media/sample-image.png, then public/test-media/sample-image-2.png, then public/test-media/sample-image-3.png.",
99
+ "Note the order the three thumbnails appear in, left to right.",
100
+ "Drag the THIRD thumbnail to the first position.",
101
+ "Fill Title 'Gallery Order Probe', Description 'Checking that gallery ordering survives a save and reload.', Category 'Beyblade Burst', Price 499, then Publish.",
102
+ "Re-open the product for editing and read the gallery order again.",
103
+ ],
104
+ inputs: {
105
+ images: "public/test-media/sample-image.png + sample-image-2.png + sample-image-3.png",
106
+ title: "Gallery Order Probe",
107
+ price: 499,
108
+ },
109
+ expectedBehaviour: "Gallery order is the seller's editorial decision about which angle a buyer sees second, so it is stored, not just displayed. Order that survives in the form but resets on reload means the array is being rebuilt from upload time rather than from the arrangement.",
110
+ expectedUiState: "After the drag the moved image is first. After publish and re-open it is STILL first, in the same order as before the save.",
111
+ expectedData: { firstGalleryImage: "public/test-media/sample-image-3.png" },
112
+ endResult: "The published product's gallery order matches what was arranged. Delete the product afterwards.",
113
+ },
114
+ "checklist-selling-product-upload-details-main-image-does-not-collide-with-gallery": {
115
+ roles: ["seller"],
116
+ startPage: "/store/products/new",
117
+ steps: [
118
+ SIGN_IN_SELLER,
119
+ "Open /store/products/new and switch to the advanced form.",
120
+ "Open the Media section and set the Main Image to public/test-media/sample-image.png, accepting the crop.",
121
+ "Add public/test-media/sample-image-2.png as the first GALLERY image.",
122
+ "Read both previews and confirm they show different pictures.",
123
+ "Publish with Title 'Slot Collision Probe', Description 'Main image and first gallery image must not overwrite each other.', Category 'Beyblade Burst', Price 499.",
124
+ "Open the product's public page and read the main image and the first gallery thumbnail.",
125
+ ],
126
+ inputs: { mainImage: "public/test-media/sample-image.png", firstGalleryImage: "public/test-media/sample-image-2.png", price: 499 },
127
+ expectedBehaviour: "The main image occupies index 1 and the gallery starts at index 2. Filenames here are content-derived with no timestamp, so two uploads that computed the same index would write the same storage path and the second would silently overwrite the first — the seller would see one of their two pictures simply vanish.",
128
+ expectedUiState: "The main image and the first gallery thumbnail show DIFFERENT pictures, both in the form and on the published page. Neither is blank.",
129
+ expectedData: { distinctImages: 2 },
130
+ endResult: "Both images survive a reload of the public page. Delete the product afterwards.",
131
+ },
132
+ "checklist-selling-product-upload-details-video-upload-succeeds": {
133
+ roles: ["seller"],
134
+ startPage: "/store/products/new",
135
+ steps: [
136
+ SIGN_IN_SELLER,
137
+ "Open /store/products/new and switch to the advanced form.",
138
+ "Open the Media section and open the video panel's 'Upload' tab.",
139
+ "Choose public/test-media/sample-video.mp4 and wait for it to finish.",
140
+ `Open the browser network panel and read the status of the ${SIGN} request.`,
141
+ "Read whether a poster frame and a duration appear.",
142
+ "Publish with Title 'Video Upload Probe', Description 'Checking the seller video upload path end to end.', Category 'Beyblade Burst', Price 499, then open the public page and play the video.",
143
+ ],
144
+ inputs: { video: "public/test-media/sample-video.mp4", price: 499 },
145
+ expectedBehaviour: "The dedicated video panel uploads through the same signed-URL flow as the gallery. This path returned HTTP 500 until 2026-09 because the panel omitted the category from the upload context and the filename generator threw on it — so every seller video upload from this panel failed while the gallery path worked. This case is that defect's regression test.",
146
+ expectedUiState: `${SIGN} returns 200, not 400 or 500. A poster frame and a duration appear. The published page plays the video.`,
147
+ expectedData: { signStatus: 200 },
148
+ endResult: "The video survives a reload of the public page. Delete the product afterwards.",
149
+ },
150
+ };
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,154 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for the selling/quick-add-minimum-details page.
3
+ * WHAT: 6 case(s), keyed by full checklist id.
4
+ *
5
+ * 🛑 THE QUICK FORM AND "THE FIELDS VISIBLE ON WHATSAPP" ARE THE SAME SET. That is
6
+ * what makes this one page rather than two.
7
+ *
8
+ * `/store/products/new` does NOT open the sectioned form. SellerProductShell.tsx
9
+ * :1339 picks `formMode = "quick"` whenever mode is create and listingType is
10
+ * standard, so the default is QuickProductForm — six fields: title*, category*,
11
+ * price*, mainImage*, description, stockQuantity. The sectioned form is behind
12
+ * "Show all fields (advanced)".
13
+ *
14
+ * And the WhatsApp catalog push (catalog-sync/route.ts:70-86) sends exactly
15
+ * title, price, mainImage, condition and stockQuantity plus a derived slug and
16
+ * link. That is the quick form's field set. A seller who fills only the short
17
+ * form has, by construction, filled everything a WhatsApp buyer will ever see.
18
+ *
19
+ * 🛑 TWO CASES HERE ARE EXPECTED TO FAIL, and their expectations are written as
20
+ * the CORRECT behaviour rather than the current one. A case that documents a bug
21
+ * as intended passes forever and locks the defect in. Both are known:
22
+ *
23
+ * condition-options-are-all-saveable the form offers Like New / Good / Fair
24
+ * (SellerProductShell.tsx:228-235) but
25
+ * productBaseSchema accepts only new /
26
+ * used / refurbished / broken
27
+ * (request-schemas.ts:155)
28
+ * description-requirement-is-honest QuickProductForm labels description
29
+ * "Brief description (optional)" and never
30
+ * validates it, while the server requires
31
+ * >= 20 characters (:122-128)
32
+ *
33
+ * @tag domain:tester
34
+ * @tag layer:seed
35
+ * @tag pattern:none
36
+ * @tag access:server-only
37
+ * @tag consumers:seed-data/authored/index.ts
38
+ * @tag sideEffects:none
39
+ */
40
+ const SIGN_IN_SELLER = "Sign in as tyson@beybladearena.in / TempPass123!.";
41
+ export const authored = {
42
+ "checklist-selling-quick-add-minimum-details-quick-form-is-the-default": {
43
+ roles: ["seller"],
44
+ startPage: "/store/products/new",
45
+ steps: [
46
+ SIGN_IN_SELLER,
47
+ "Open /store/products/new without clicking anything else.",
48
+ "Read the heading and count the input fields on screen.",
49
+ "List the field labels in order.",
50
+ "Read whether a control offering the full form is present, and read its label.",
51
+ "Open /store/art/new and count its fields for comparison.",
52
+ ],
53
+ expectedBehaviour: "Creating an ordinary product opens the SHORT form, because most listings need six fields and the sectioned form asks for thirty. The full form stays one click away rather than being the default. Non-standard types (art, auctions) open the sectioned form directly, since they carry type-specific fields the short form has no room for.",
54
+ expectedUiState: "Six fields: Product Name, Category, Price, Product Image, Description, Stock Quantity — and a control reading 'Show all fields (advanced)'. /store/art/new shows the sectioned form instead.",
55
+ expectedData: { quickFormFields: 6 },
56
+ endResult: "Read-only; leave without saving.",
57
+ },
58
+ "checklist-selling-quick-add-minimum-details-publish-with-the-minimum": {
59
+ roles: ["seller"],
60
+ startPage: "/store/products/new",
61
+ steps: [
62
+ SIGN_IN_SELLER,
63
+ "Open /store/products/new.",
64
+ "Enter Product Name 'Minimum Details Probe'.",
65
+ "Pick Category 'Beyblade Burst'.",
66
+ "Enter Price 499.",
67
+ "Upload public/test-media/sample-image.png as the Product Image.",
68
+ "Enter Description 'A short-form listing created with only the required fields present.'",
69
+ "Click Publish and read what happens.",
70
+ "Open the product's public page and read the title, price and image.",
71
+ ],
72
+ inputs: {
73
+ title: "Minimum Details Probe",
74
+ category: "Beyblade Burst",
75
+ price: 499,
76
+ image: "public/test-media/sample-image.png",
77
+ description: "A short-form listing created with only the required fields present.",
78
+ },
79
+ expectedBehaviour: "The short form can produce a real, published, buyable listing on its own. If it cannot, it is a decoy: the seller fills it, is refused, and has to learn the long form anyway — having already done the work twice.",
80
+ expectedUiState: "Publish succeeds with no inline errors and no error toast. The public page renders the title, ₹499 and the uploaded image.",
81
+ expectedData: { published: true },
82
+ endResult: "The listing survives a reload of the public page and appears in /store/products. Delete it afterwards.",
83
+ },
84
+ "checklist-selling-quick-add-minimum-details-whatsapp-shows-the-same-fields": {
85
+ roles: ["seller"],
86
+ startPage: "/store/whatsapp",
87
+ steps: [
88
+ SIGN_IN_SELLER,
89
+ "Publish 'Minimum Details Probe' from /store/products/new as in the previous case, with Price 499.",
90
+ "Open /store/whatsapp and find the catalog preview.",
91
+ "Locate the tile for 'Minimum Details Probe'.",
92
+ "Read every piece of information the tile shows.",
93
+ "Compare that list against the fields the quick form asked for.",
94
+ ],
95
+ inputs: { title: "Minimum Details Probe", price: 499 },
96
+ expectedBehaviour: "What WhatsApp shows is a subset of what the short form collects — title, price, image, condition and stock. This is the reason the short form is the right shape: a seller who fills only it has already supplied everything a WhatsApp buyer will see, so nothing is missing from the channel most of them arrive through.",
97
+ expectedUiState: "The tile shows the image, the name and ₹499. Nothing on it is blank or a placeholder, and nothing it shows was absent from the quick form.",
98
+ endResult: "Read-only against the catalog preview. Delete the probe listing afterwards.",
99
+ },
100
+ "checklist-selling-quick-add-minimum-details-advanced-flip-keeps-values": {
101
+ roles: ["seller"],
102
+ startPage: "/store/products/new",
103
+ steps: [
104
+ SIGN_IN_SELLER,
105
+ "Open /store/products/new.",
106
+ "Enter Product Name 'Flip Probe', Price 750, and Description 'Checking that switching to the full form keeps what was already typed.'",
107
+ "Pick Category 'Beyblade Burst'.",
108
+ "Click 'Show all fields (advanced)'.",
109
+ "Read the Title, Price, Description and Category fields in the sectioned form.",
110
+ "Read whether any value is blank that was filled a moment ago.",
111
+ ],
112
+ inputs: { title: "Flip Probe", price: 750, category: "Beyblade Burst" },
113
+ expectedBehaviour: "The flip changes the presentation, not the draft. Losing typed values on the way to the full form punishes the seller for outgrowing the short one, and teaches them to start with the long form every time — which defeats the point of having a short one.",
114
+ expectedUiState: "Title reads 'Flip Probe', Price 750, Category 'Beyblade Burst' and the description is intact in the sectioned form's Basic and Pricing sections.",
115
+ expectedData: { valuesPreserved: 4 },
116
+ endResult: "Leave without saving.",
117
+ },
118
+ "checklist-selling-quick-add-minimum-details-condition-options-are-all-saveable": {
119
+ roles: ["seller"],
120
+ startPage: "/store/products/new",
121
+ steps: [
122
+ SIGN_IN_SELLER,
123
+ "Open /store/products/new and click 'Show all fields (advanced)'.",
124
+ "Open the Basic section and read every option in the Condition dropdown, writing them all down.",
125
+ "Fill Title 'Condition Probe', Description 'Checking that every condition the form offers can actually be saved.', Category 'Beyblade Burst', Price 499 and a Main Image.",
126
+ "Select Condition 'Like New' and click Publish.",
127
+ "Read the result — inline error, toast, or success.",
128
+ "Repeat with 'Good', then with 'Fair', then with 'New', reading the result each time.",
129
+ ],
130
+ inputs: { title: "Condition Probe", price: 499, conditions: "Like New, Good, Fair, New" },
131
+ expectedBehaviour: "Every option the dropdown offers can be saved. A select that lists a value the server rejects is a trap with no way out — the seller picks the option that describes their item honestly and is refused, with nothing on screen saying which options are real.",
132
+ expectedUiState: "Publishing succeeds for EVERY option in the list, including Like New, Good and Fair. Any option that produces a validation failure is the finding — record which ones and quote the message.",
133
+ expectedData: { saveableConditions: "all options listed in the dropdown" },
134
+ endResult: "Each successful publish is visible on the public page with the chosen condition. Delete the listings afterwards.",
135
+ },
136
+ "checklist-selling-quick-add-minimum-details-description-requirement-is-honest": {
137
+ roles: ["seller"],
138
+ startPage: "/store/products/new",
139
+ steps: [
140
+ SIGN_IN_SELLER,
141
+ "Open /store/products/new.",
142
+ "Read the Description field's label and note exactly whether it says required or optional.",
143
+ "Fill Product Name 'Short Description Probe', Category 'Beyblade Burst', Price 499 and a Product Image.",
144
+ "Type exactly 'Nice bey' in Description — eight characters.",
145
+ "Click Publish and read what happens, and WHERE it appears.",
146
+ "If it was refused, read whether the message appeared on the Description field itself or somewhere else.",
147
+ ],
148
+ inputs: { title: "Short Description Probe", description: "Nice bey", price: 499 },
149
+ expectedBehaviour: "The form tells the truth about what it requires, and it tells it at the field. Either the description really is optional and a short one publishes, or it is required and the label says so with the minimum length stated. What must not happen is a field marked optional being refused by the server after the seller commits — that is the form making a promise the save does not keep.",
150
+ expectedUiState: "Label and behaviour agree. If a minimum length applies, the label states it and a short entry is caught inline on the Description field BEFORE publishing. A toast appearing after Publish, with the field itself showing no error, is the failure.",
151
+ expectedData: { errorShownInline: true },
152
+ endResult: "Either the listing publishes, or nothing is created and the form still holds what was typed. Delete anything created.",
153
+ },
154
+ };
@@ -176,6 +176,8 @@ export const authored = {
176
176
  endResult: "Read-only; nothing persists.",
177
177
  },
178
178
  "checklist-selling-seller-orders-seller-new-order-notification-reaches-seller": {
179
+ requiresHumanChannel: true,
180
+ humanChannelReason: "real email inbox",
179
181
  roles: ["buyer", "seller"],
180
182
  startPage: "/products/product-beyblade-burst-valkyrie",
181
183
  steps: [
@@ -9,3 +9,4 @@ export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
9
  export { cartsTesterSeedData } from "./carts-tester-seed-data";
10
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
11
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
12
+ export { testerResponsesSeedData } from "./tester-responses-seed-data";
@@ -9,3 +9,4 @@ export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
9
  export { cartsTesterSeedData } from "./carts-tester-seed-data";
10
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
11
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
12
+ export { testerResponsesSeedData } from "./tester-responses-seed-data";