@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.1

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 (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. package/tsconfig.json +0 -7
@@ -1,1111 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { Offer, Product } from "@decocms/apps-commerce/types";
3
- import {
4
- aggregateOffers,
5
- bestOfferFirst,
6
- categoryTreeToNavbar,
7
- filtersFromURL,
8
- filtersToSearchParams,
9
- forceHttpsOnAssets,
10
- inStock,
11
- legacyFacetsFromURL,
12
- legacyFacetsNormalize,
13
- mergeFacets,
14
- normalizeFacet,
15
- parsePageType,
16
- pickSku,
17
- SCHEMA_IN_STOCK,
18
- SCHEMA_OUT_OF_STOCK,
19
- sortProducts,
20
- toAdditionalPropertyCategory,
21
- toAdditionalPropertyCluster,
22
- toAdditionalPropertyReferenceId,
23
- toAdditionalPropertySpecification,
24
- toBrand,
25
- toPostalAddress,
26
- toProduct,
27
- toProductShelf,
28
- toProductVariant,
29
- } from "../transform";
30
-
31
- const makeOffer = (price: number, availability: string): Offer => ({
32
- "@type": "Offer",
33
- price,
34
- availability: availability as Offer["availability"],
35
- priceSpecification: [],
36
- inventoryLevel: { value: 1 },
37
- });
38
-
39
- describe("inStock", () => {
40
- it("returns true when in stock", () => {
41
- expect(inStock(makeOffer(10, SCHEMA_IN_STOCK))).toBe(true);
42
- });
43
-
44
- it("returns false when out of stock", () => {
45
- expect(inStock(makeOffer(10, SCHEMA_OUT_OF_STOCK))).toBe(false);
46
- });
47
- });
48
-
49
- describe("bestOfferFirst", () => {
50
- it("sorts in-stock before out-of-stock", () => {
51
- const inStockOffer = makeOffer(100, SCHEMA_IN_STOCK);
52
- const outOfStockOffer = makeOffer(10, SCHEMA_OUT_OF_STOCK);
53
- expect(bestOfferFirst(inStockOffer, outOfStockOffer)).toBe(-1);
54
- expect(bestOfferFirst(outOfStockOffer, inStockOffer)).toBe(1);
55
- });
56
-
57
- it("sorts by price when both in stock", () => {
58
- const cheap = makeOffer(10, SCHEMA_IN_STOCK);
59
- const expensive = makeOffer(100, SCHEMA_IN_STOCK);
60
- expect(bestOfferFirst(cheap, expensive)).toBeLessThan(0);
61
- expect(bestOfferFirst(expensive, cheap)).toBeGreaterThan(0);
62
- });
63
-
64
- it("returns 0 for equal offers", () => {
65
- const a = makeOffer(50, SCHEMA_IN_STOCK);
66
- const b = makeOffer(50, SCHEMA_IN_STOCK);
67
- expect(bestOfferFirst(a, b)).toBe(0);
68
- });
69
- });
70
-
71
- describe("aggregateOffers", () => {
72
- it("returns undefined for empty array", () => {
73
- expect(aggregateOffers([])).toBeUndefined();
74
- });
75
-
76
- it("aggregates single offer", () => {
77
- const offer = makeOffer(100, SCHEMA_IN_STOCK);
78
- const result = aggregateOffers([offer], "BRL");
79
- expect(result).toEqual({
80
- "@type": "AggregateOffer",
81
- priceCurrency: "BRL",
82
- highPrice: 100,
83
- lowPrice: 100,
84
- offerCount: 1,
85
- offers: [offer],
86
- });
87
- });
88
-
89
- it("finds low and high prices across multiple offers", () => {
90
- const offers = [
91
- makeOffer(50, SCHEMA_IN_STOCK),
92
- makeOffer(100, SCHEMA_IN_STOCK),
93
- makeOffer(25, SCHEMA_IN_STOCK),
94
- ];
95
- const result = aggregateOffers(offers, "BRL");
96
- expect(result?.lowPrice).toBe(25);
97
- expect(result?.highPrice).toBe(100);
98
- expect(result?.offerCount).toBe(3);
99
- });
100
-
101
- it("ignores out-of-stock offers for high price", () => {
102
- const offers = [makeOffer(25, SCHEMA_IN_STOCK), makeOffer(200, SCHEMA_OUT_OF_STOCK)];
103
- const result = aggregateOffers(offers, "BRL");
104
- expect(result?.highPrice).toBe(25);
105
- });
106
- });
107
-
108
- describe("pickSku", () => {
109
- const makeProduct = (
110
- items: Array<{
111
- itemId: string;
112
- sellers: Array<{ commertialOffer: { AvailableQuantity: number } }>;
113
- }>,
114
- ) => ({ items, origin: "intelligent-search" }) as any;
115
-
116
- it("returns specified SKU", () => {
117
- const product = makeProduct([
118
- { itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
119
- { itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
120
- ]);
121
- expect(pickSku(product, "2").itemId).toBe("2");
122
- });
123
-
124
- it("returns first available SKU when no ID specified", () => {
125
- const product = makeProduct([
126
- { itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
127
- { itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
128
- ]);
129
- expect(pickSku(product).itemId).toBe("2");
130
- });
131
-
132
- it("falls back to first SKU when none available", () => {
133
- const product = makeProduct([
134
- { itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
135
- { itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
136
- ]);
137
- expect(pickSku(product).itemId).toBe("1");
138
- });
139
-
140
- it("falls back to first SKU when specified ID not found", () => {
141
- const product = makeProduct([
142
- { itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
143
- ]);
144
- expect(pickSku(product, "999").itemId).toBe("1");
145
- });
146
- });
147
-
148
- describe("toAdditionalPropertyCategory", () => {
149
- it("creates category property value", () => {
150
- const result = toAdditionalPropertyCategory({ propertyID: "123", value: "Shoes" });
151
- expect(result).toEqual({
152
- "@type": "PropertyValue",
153
- name: "category",
154
- propertyID: "123",
155
- value: "Shoes",
156
- });
157
- });
158
- });
159
-
160
- describe("toAdditionalPropertyCluster", () => {
161
- it("creates cluster property value", () => {
162
- const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" });
163
- expect(result).toEqual({
164
- "@type": "PropertyValue",
165
- name: "cluster",
166
- propertyID: "456",
167
- value: "Sale",
168
- description: undefined,
169
- });
170
- });
171
-
172
- it("marks as highlight when in set", () => {
173
- const highlights = new Set(["456"]);
174
- const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" }, highlights);
175
- expect(result.description).toBe("highlight");
176
- });
177
-
178
- it("does not mark as highlight when not in set", () => {
179
- const highlights = new Set(["789"]);
180
- const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" }, highlights);
181
- expect(result.description).toBeUndefined();
182
- });
183
- });
184
-
185
- describe("toAdditionalPropertyReferenceId", () => {
186
- it("creates reference ID property value", () => {
187
- const result = toAdditionalPropertyReferenceId({ name: "RefId", value: "ABC123" });
188
- expect(result).toEqual({
189
- "@type": "PropertyValue",
190
- name: "RefId",
191
- value: "ABC123",
192
- valueReference: "ReferenceID",
193
- });
194
- });
195
- });
196
-
197
- describe("toAdditionalPropertySpecification", () => {
198
- it("creates specification property value", () => {
199
- const result = toAdditionalPropertySpecification({ name: "Color", value: "Red" });
200
- expect(result).toEqual({
201
- "@type": "PropertyValue",
202
- name: "Color",
203
- value: "Red",
204
- propertyID: undefined,
205
- valueReference: "SPECIFICATION",
206
- });
207
- });
208
-
209
- it("includes propertyID when provided", () => {
210
- const result = toAdditionalPropertySpecification({
211
- name: "Color",
212
- value: "Red",
213
- propertyID: "group1",
214
- });
215
- expect(result.propertyID).toBe("group1");
216
- });
217
- });
218
-
219
- describe("filtersToSearchParams", () => {
220
- it("converts facets to search params", () => {
221
- const facets = [
222
- { key: "category", value: "shoes" },
223
- { key: "brand", value: "nike" },
224
- ];
225
- const params = filtersToSearchParams(facets);
226
- expect(params.get("filter.category")).toBe("shoes");
227
- expect(params.get("filter.brand")).toBe("nike");
228
- });
229
-
230
- it("preserves existing params", () => {
231
- const existing = new URLSearchParams("page=1");
232
- const params = filtersToSearchParams([{ key: "brand", value: "nike" }], existing);
233
- expect(params.get("page")).toBe("1");
234
- expect(params.get("filter.brand")).toBe("nike");
235
- });
236
- });
237
-
238
- describe("legacyFacetsNormalize", () => {
239
- it("normalizes legacy price format", () => {
240
- const result = legacyFacetsNormalize("priceFrom", "de-34,90-a-56,90");
241
- expect(result).toEqual({ key: "price", value: "34.90:56.90" });
242
- });
243
-
244
- it("maps legacy key names", () => {
245
- const result = legacyFacetsNormalize("productClusterSearchableIds", "123");
246
- expect(result).toEqual({ key: "productClusterIds", value: "123" });
247
- });
248
-
249
- it("passes through unknown keys", () => {
250
- const result = legacyFacetsNormalize("brand", "nike");
251
- expect(result).toEqual({ key: "brand", value: "nike" });
252
- });
253
- });
254
-
255
- describe("legacyFacetsFromURL", () => {
256
- it("extracts facets from URL with map param", () => {
257
- const url = new URL("https://example.com/shoes/nike?map=c,brand");
258
- const result = legacyFacetsFromURL(url);
259
- expect(result).toEqual([
260
- { key: "c", value: "shoes" },
261
- { key: "brand", value: "nike" },
262
- ]);
263
- });
264
-
265
- it("returns empty array when no map param", () => {
266
- const url = new URL("https://example.com/shoes");
267
- const result = legacyFacetsFromURL(url);
268
- expect(result).toEqual([]);
269
- });
270
-
271
- it("handles mismatched lengths", () => {
272
- const url = new URL("https://example.com/shoes?map=c,brand,extra");
273
- const result = legacyFacetsFromURL(url);
274
- expect(result).toHaveLength(1);
275
- });
276
- });
277
-
278
- describe("filtersFromURL", () => {
279
- it("extracts both legacy and filter params", () => {
280
- const url = new URL("https://example.com/shoes?map=c&filter.brand=nike");
281
- const result = filtersFromURL(url);
282
- expect(result).toEqual([
283
- { key: "c", value: "shoes" },
284
- { key: "brand", value: "nike" },
285
- ]);
286
- });
287
-
288
- it("extracts only filter params when no map", () => {
289
- const url = new URL("https://example.com/?filter.brand=nike&filter.category=shoes");
290
- const result = filtersFromURL(url);
291
- expect(result).toEqual([
292
- { key: "brand", value: "nike" },
293
- { key: "category", value: "shoes" },
294
- ]);
295
- });
296
- });
297
-
298
- describe("mergeFacets", () => {
299
- it("merges two facet arrays", () => {
300
- const f1 = [{ key: "brand", value: "nike" }];
301
- const f2 = [{ key: "category", value: "shoes" }];
302
- const result = mergeFacets(f1, f2);
303
- expect(result).toHaveLength(2);
304
- });
305
-
306
- it("deduplicates facets", () => {
307
- const f1 = [{ key: "brand", value: "nike" }];
308
- const f2 = [{ key: "brand", value: "nike" }];
309
- const result = mergeFacets(f1, f2);
310
- expect(result).toHaveLength(1);
311
- });
312
-
313
- it("keeps both when same key different value", () => {
314
- const f1 = [{ key: "brand", value: "nike" }];
315
- const f2 = [{ key: "brand", value: "adidas" }];
316
- const result = mergeFacets(f1, f2);
317
- expect(result).toHaveLength(2);
318
- });
319
- });
320
-
321
- describe("categoryTreeToNavbar", () => {
322
- it("transforms tree to navbar elements", () => {
323
- const tree = [
324
- {
325
- id: 1,
326
- name: "Shoes",
327
- hasChildren: true,
328
- url: "https://example.com/shoes",
329
- children: [
330
- {
331
- id: 2,
332
- name: "Running",
333
- hasChildren: false,
334
- url: "https://example.com/shoes/running",
335
- children: [],
336
- },
337
- ],
338
- },
339
- ];
340
- const result = categoryTreeToNavbar(tree);
341
- expect(result).toEqual([
342
- {
343
- "@type": "SiteNavigationElement",
344
- url: "/shoes",
345
- name: "Shoes",
346
- children: [
347
- {
348
- "@type": "SiteNavigationElement",
349
- url: "/shoes/running",
350
- name: "Running",
351
- children: [],
352
- },
353
- ],
354
- },
355
- ]);
356
- });
357
-
358
- it("returns empty array for empty tree", () => {
359
- expect(categoryTreeToNavbar([])).toEqual([]);
360
- });
361
- });
362
-
363
- describe("toBrand", () => {
364
- it("transforms VTEX brand", () => {
365
- const brand = {
366
- id: 1,
367
- name: "Nike",
368
- imageUrl: "/brands/nike.png",
369
- metaTagDescription: "Nike brand",
370
- };
371
- const result = toBrand(brand as any, "https://example.com");
372
- expect(result).toEqual({
373
- "@type": "Brand",
374
- "@id": "1",
375
- name: "Nike",
376
- logo: "https://example.com/brands/nike.png",
377
- description: "Nike brand",
378
- });
379
- });
380
-
381
- it("keeps absolute URLs as-is", () => {
382
- const brand = {
383
- id: 1,
384
- name: "Nike",
385
- imageUrl: "https://cdn.example.com/nike.png",
386
- metaTagDescription: "",
387
- };
388
- const result = toBrand(brand as any, "https://example.com");
389
- expect(result.logo).toBe("https://cdn.example.com/nike.png");
390
- });
391
- });
392
-
393
- describe("normalizeFacet", () => {
394
- it("sets Map to priceFrom and Value to Slug", () => {
395
- const facet = { Map: "c", Value: "shoes", Slug: "de-10-a-50", Name: "Price", Quantity: 5 };
396
- const result = normalizeFacet(facet as any);
397
- expect(result.Map).toBe("priceFrom");
398
- expect(result.Value).toBe("de-10-a-50");
399
- });
400
- });
401
-
402
- describe("sortProducts", () => {
403
- it("sorts products by specified order", () => {
404
- const products = [
405
- { "@type": "Product", sku: "3" },
406
- { "@type": "Product", sku: "1" },
407
- { "@type": "Product", sku: "2" },
408
- ] as unknown as Product[];
409
- const result = sortProducts(products, ["1", "2", "3"], "sku");
410
- expect(result.map((p) => p.sku)).toEqual(["1", "2", "3"]);
411
- });
412
-
413
- it("returns undefined for missing IDs", () => {
414
- const products = [{ "@type": "Product", sku: "1" }] as unknown as Product[];
415
- const result = sortProducts(products, ["1", "999"], "sku");
416
- expect(result[0].sku).toBe("1");
417
- expect(result[1]).toBeUndefined();
418
- });
419
- });
420
-
421
- describe("parsePageType", () => {
422
- it("maps FullText to Search", () => {
423
- expect(parsePageType({ pageType: "FullText" } as any)).toBe("Search");
424
- });
425
-
426
- it("maps NotFound to Unknown", () => {
427
- expect(parsePageType({ pageType: "NotFound" } as any)).toBe("Unknown");
428
- });
429
-
430
- it("passes through other types", () => {
431
- expect(parsePageType({ pageType: "Department" } as any)).toBe("Department");
432
- expect(parsePageType({ pageType: "Brand" } as any)).toBe("Brand");
433
- });
434
- });
435
-
436
- describe("forceHttpsOnAssets", () => {
437
- it("converts http to https on item images", () => {
438
- const orderForm = {
439
- items: [
440
- { imageUrl: "http://example.com/img.jpg" },
441
- { imageUrl: "https://example.com/img2.jpg" },
442
- ],
443
- };
444
- const result = forceHttpsOnAssets(orderForm as any);
445
- expect(result.items[0].imageUrl).toBe("https://example.com/img.jpg");
446
- expect(result.items[1].imageUrl).toBe("https://example.com/img2.jpg");
447
- });
448
-
449
- it("handles items without imageUrl", () => {
450
- const orderForm = { items: [{ imageUrl: undefined }] };
451
- const result = forceHttpsOnAssets(orderForm as any);
452
- expect(result.items[0].imageUrl).toBeUndefined();
453
- });
454
- });
455
-
456
- describe("toPostalAddress", () => {
457
- it("transforms VTEX address to PostalAddress", () => {
458
- const address = {
459
- addressId: "addr1",
460
- country: "BRA",
461
- city: "São Paulo",
462
- state: "SP",
463
- neighborhood: "Pinheiros",
464
- postalCode: "05422-000",
465
- street: "Rua dos Pinheiros",
466
- number: "123",
467
- addressName: "Home",
468
- receiverName: "John Doe",
469
- complement: "Apt 1",
470
- reference: "Near the park",
471
- geoCoordinates: [-23.5668, -46.6901],
472
- };
473
- const result = toPostalAddress(address as any);
474
- expect(result["@type"]).toBe("PostalAddress");
475
- expect(result["@id"]).toBe("addr1");
476
- expect(result.addressCountry).toBe("BRA");
477
- expect(result.addressLocality).toBe("São Paulo");
478
- expect(result.addressRegion).toBe("SP");
479
- expect(result.postalCode).toBe("05422-000");
480
- expect(result.streetAddress).toBe("Rua dos Pinheiros");
481
- expect(result.identifier).toBe("123");
482
- expect(result.name).toBe("Home");
483
- expect(result.alternateName).toBe("John Doe");
484
- });
485
-
486
- it("returns undefined for empty optional fields", () => {
487
- const address = {
488
- addressId: "addr1",
489
- country: "BRA",
490
- city: "SP",
491
- state: "SP",
492
- postalCode: "05422-000",
493
- street: "Rua X",
494
- };
495
- const result = toPostalAddress(address as any);
496
- expect(result.areaServed).toBeUndefined();
497
- expect(result.identifier).toBeUndefined();
498
- expect(result.name).toBeUndefined();
499
- });
500
- });
501
-
502
- describe("toProductVariant", () => {
503
- const makeISProduct = (overrides: Record<string, unknown> = {}) =>
504
- ({
505
- origin: "intelligent-search",
506
- productId: "PROD1",
507
- productName: "Test Product",
508
- brand: "TestBrand",
509
- brandId: 1,
510
- brandImageUrl: null,
511
- productReference: "REF1",
512
- description: "Full description HTML",
513
- releaseDate: "2024-01-01",
514
- linkText: "test-product",
515
- categories: ["/Electronics/TVs/"],
516
- categoriesIds: ["/1/2/"],
517
- categoryId: "2",
518
- productClusters: { "100": "Sale" },
519
- clusterHighlights: {},
520
- items: [],
521
- ...overrides,
522
- }) as any;
523
-
524
- const makeISSku = (overrides: Record<string, unknown> = {}) =>
525
- ({
526
- itemId: "SKU1",
527
- name: "Test SKU",
528
- ean: "1234567890123",
529
- referenceId: [{ Key: "RefId", Value: "REF-SKU1" }],
530
- images: [
531
- { imageUrl: "https://img.com/1.jpg", imageText: "Front", imageLabel: "front" },
532
- { imageUrl: "https://img.com/2.jpg", imageText: "Back", imageLabel: "back" },
533
- { imageUrl: "https://img.com/3.jpg", imageText: "Side", imageLabel: "side" },
534
- ],
535
- videos: ["https://video.com/1.mp4"],
536
- sellers: [
537
- {
538
- sellerId: "1",
539
- sellerName: "Seller One",
540
- commertialOffer: {
541
- AvailableQuantity: 10,
542
- Price: 99.9,
543
- ListPrice: 129.9,
544
- spotPrice: 89.9,
545
- PriceValidUntil: "2025-12-31",
546
- Installments: [
547
- {
548
- Value: 33.3,
549
- NumberOfInstallments: 3,
550
- Name: "Visa",
551
- InterestRate: 0,
552
- TotalValuePlusInterestRate: 99.9,
553
- PaymentSystemName: "Visa",
554
- },
555
- ],
556
- GiftSkuIds: [],
557
- teasers: [],
558
- },
559
- },
560
- ],
561
- variations: [
562
- { name: "Cor", values: ["Preto"] },
563
- { name: "Voltagem", values: ["220V"] },
564
- { name: "Tamanho", values: ["G"] },
565
- ],
566
- kitItems: [],
567
- complementName: "Complement",
568
- estimatedDateArrival: null,
569
- modalType: null,
570
- ...overrides,
571
- }) as any;
572
-
573
- const baseOptions = {
574
- baseUrl: "https://example.com",
575
- priceCurrency: "BRL",
576
- };
577
-
578
- it("returns minimal product shape", () => {
579
- const product = makeISProduct({ items: [makeISSku()] });
580
- const sku = makeISSku();
581
- const result = toProductVariant(product, sku, baseOptions);
582
-
583
- expect(result["@type"]).toBe("Product");
584
- expect(result.productID).toBe("SKU1");
585
- expect(result.sku).toBe("SKU1");
586
- expect(result.name).toBe("Test SKU");
587
- expect(result.url).toContain("/test-product/p");
588
- expect(result.inProductGroupWithID).toBe("PROD1");
589
- });
590
-
591
- it("drops description, video, brand, gtin, releaseDate, isVariantOf", () => {
592
- const product = makeISProduct({ items: [makeISSku()] });
593
- const sku = makeISSku();
594
- const result = toProductVariant(product, sku, baseOptions);
595
-
596
- expect(result.video).toBeUndefined();
597
- expect(result.description).toBeUndefined();
598
- expect(result.brand).toBeUndefined();
599
- expect(result.gtin).toBeUndefined();
600
- expect(result.releaseDate).toBeUndefined();
601
- expect(result.alternateName).toBeUndefined();
602
- expect(result.isVariantOf).toBeUndefined();
603
- expect(result.isAccessoryOrSparePartFor).toBeUndefined();
604
- expect(result.category).toBeUndefined();
605
- });
606
-
607
- it("includes image[0] by default — selectors render thumbnails from it", () => {
608
- const product = makeISProduct({ items: [makeISSku()] });
609
- const sku = makeISSku();
610
- const result = toProductVariant(product, sku, baseOptions);
611
-
612
- expect(result.image).toHaveLength(1);
613
- expect(result.image?.[0]).toMatchObject({
614
- "@type": "ImageObject",
615
- url: "https://img.com/1.jpg",
616
- encodingFormat: "image",
617
- });
618
- });
619
-
620
- it("includes real inventoryLevel by default — selectors gate stock state on it", () => {
621
- const product = makeISProduct({ items: [makeISSku()] });
622
- const sku = makeISSku();
623
- const result = toProductVariant(product, sku, baseOptions);
624
-
625
- const offer = result.offers!.offers[0];
626
- expect(offer.inventoryLevel?.value).toBe(10);
627
- });
628
-
629
- it("drops image when variantIncludeImage: false", () => {
630
- const product = makeISProduct({ items: [makeISSku()] });
631
- const sku = makeISSku();
632
- const result = toProductVariant(product, sku, {
633
- ...baseOptions,
634
- variantIncludeImage: false,
635
- });
636
-
637
- expect(result.image).toBeUndefined();
638
- });
639
-
640
- it("zeros inventoryLevel when variantIncludeInventory: false (legacy lean behavior)", () => {
641
- const product = makeISProduct({ items: [makeISSku()] });
642
- const sku = makeISSku();
643
- const result = toProductVariant(product, sku, {
644
- ...baseOptions,
645
- variantIncludeInventory: false,
646
- });
647
-
648
- const offer = result.offers!.offers[0];
649
- expect(offer.inventoryLevel?.value).toBe(0);
650
- });
651
-
652
- it("filters spec additionalProperty to variant-differentiating names only", () => {
653
- const product = makeISProduct({ items: [makeISSku()] });
654
- const sku = makeISSku();
655
- const result = toProductVariant(product, sku, baseOptions);
656
-
657
- const specNames = (result.additionalProperty ?? [])
658
- .filter((p) => p.valueReference !== "ReferenceID")
659
- .map((p) => p.name);
660
- // Should only contain Cor, Voltagem, Tamanho (from VARIANT_PROPERTY_NAMES)
661
- for (const name of specNames) {
662
- expect(["Cor", "Voltagem", "Tamanho"]).toContain(name);
663
- }
664
- expect(specNames.length).toBeGreaterThan(0);
665
- });
666
-
667
- it("respects custom variantPropertyNames", () => {
668
- const product = makeISProduct({ items: [makeISSku()] });
669
- const sku = makeISSku();
670
- const result = toProductVariant(product, sku, {
671
- ...baseOptions,
672
- variantPropertyNames: new Set(["Cor"]),
673
- });
674
-
675
- const specNames = (result.additionalProperty ?? [])
676
- .filter((p) => p.valueReference !== "ReferenceID")
677
- .map((p) => p.name);
678
- expect(specNames).toEqual(["Cor"]);
679
- });
680
-
681
- // Analytics reads the SKU RefId off additionalProperty for GA4 dimension2
682
- // (view_item_list / select_item) and its productImpression / productClick
683
- // equivalents. It lives in `referenceId`, not `variations`, so the
684
- // variantProps filter used to drop it and blank the dimension.
685
- it("keeps the SKU reference ids (RefId) alongside the variant specs", () => {
686
- const product = makeISProduct({ items: [makeISSku()] });
687
- const sku = { ...makeISSku(), referenceId: [{ Key: "RefId", Value: "010276949803" }] };
688
- const result = toProductVariant(product, sku, baseOptions);
689
-
690
- const refId = result.additionalProperty?.find((p) => p.name === "RefId");
691
- expect(refId?.value).toBe("010276949803");
692
- expect(refId?.valueReference).toBe("ReferenceID");
693
- });
694
-
695
- it("omits RefId when the SKU carries no referenceId", () => {
696
- const product = makeISProduct({ items: [makeISSku()] });
697
- const sku = { ...makeISSku(), referenceId: undefined };
698
- const result = toProductVariant(product, sku, baseOptions);
699
-
700
- expect(result.additionalProperty?.some((p) => p.name === "RefId")).toBe(false);
701
- });
702
-
703
- it("produces lean offers with availability but no priceSpecification details", () => {
704
- const product = makeISProduct({ items: [makeISSku()] });
705
- const sku = makeISSku();
706
- const result = toProductVariant(product, sku, baseOptions);
707
-
708
- expect(result.offers).toBeDefined();
709
- expect(result.offers?.offers).toHaveLength(1);
710
-
711
- const offer = result.offers!.offers[0];
712
- expect(offer.availability).toBe("https://schema.org/InStock");
713
- expect(offer.seller).toBe("1");
714
- expect(offer.priceSpecification).toEqual([]);
715
- });
716
-
717
- it("handles SKU with no sellers", () => {
718
- const product = makeISProduct({ items: [makeISSku({ sellers: [] })] });
719
- const sku = makeISSku({ sellers: [] });
720
- const result = toProductVariant(product, sku, baseOptions);
721
-
722
- // Should still return a valid product, offers may be undefined (no sellers)
723
- expect(result["@type"]).toBe("Product");
724
- expect(result.productID).toBe("SKU1");
725
- });
726
- });
727
-
728
- describe("toProductShelf", () => {
729
- const makeSku = (overrides: Record<string, unknown> = {}) =>
730
- ({
731
- itemId: "SKU1",
732
- name: "Test SKU",
733
- referenceId: [{ Key: "RefId", Value: "REF-SKU1" }],
734
- images: [{ imageUrl: "https://img.com/1.jpg", imageText: "Front", imageLabel: "front" }],
735
- sellers: [
736
- {
737
- sellerId: "1",
738
- sellerName: "Seller One",
739
- commertialOffer: {
740
- AvailableQuantity: 10,
741
- Price: 99.9,
742
- ListPrice: 129.9,
743
- spotPrice: 89.9,
744
- PriceValidUntil: "2025-12-31",
745
- Installments: [],
746
- GiftSkuIds: [],
747
- teasers: [],
748
- },
749
- },
750
- ],
751
- variations: [{ name: "Cor", values: ["Preto"] }],
752
- ...overrides,
753
- }) as any;
754
-
755
- // Campanha is a PRODUCT specification (in specificationGroups), NOT a sku.variation.
756
- // It is the marker one storefront uses to tag launches ("Lançamentos").
757
- const makeProduct = (overrides: Record<string, unknown> = {}) =>
758
- ({
759
- origin: "intelligent-search",
760
- productId: "PROD1",
761
- productName: "Test Product",
762
- brand: "TestBrand",
763
- linkText: "test-product",
764
- categories: ["/Electronics/"],
765
- categoriesIds: ["/1/"],
766
- categoryId: "1",
767
- productClusters: [{ id: "100", name: "Sale" }],
768
- clusterHighlights: [],
769
- specificationGroups: [
770
- { name: "Campanha", specifications: [{ name: "Campanha", values: ["Lançamentos"] }] },
771
- ],
772
- items: [makeSku()],
773
- ...overrides,
774
- }) as any;
775
-
776
- const options = { baseUrl: "https://example.com", priceCurrency: "BRL" };
777
-
778
- it("surfaces the Campanha launch spec in isVariantOf.additionalProperty (ReleaseFlag reads it)", () => {
779
- const result = toProductShelf(makeProduct(), makeSku(), 0, options);
780
- const groupProps = result.isVariantOf?.additionalProperty ?? [];
781
- expect(groupProps).toEqual(
782
- expect.arrayContaining([
783
- expect.objectContaining({ name: "Campanha", value: "Lançamentos" }),
784
- ]),
785
- );
786
- });
787
-
788
- it("leaves isVariantOf.additionalProperty empty when there is no shelf-safe group spec", () => {
789
- const result = toProductShelf(
790
- makeProduct({ specificationGroups: [] }),
791
- makeSku(),
792
- 0,
793
- options,
794
- );
795
- expect(result.isVariantOf?.additionalProperty).toEqual([]);
796
- });
797
-
798
- // Analytics reads the product reference off isVariantOf.model for GA4
799
- // dimension1 (view_item_list / select_item) and its productImpression /
800
- // productClick equivalents. toProductShelf used to drop it, blanking the
801
- // dimension on every shelf, PLP and search result.
802
- it("carries productReference on isVariantOf.model (analytics dimension1)", () => {
803
- const result = toProductShelf(
804
- makeProduct({ productReference: "0102769498" }),
805
- makeSku(),
806
- 0,
807
- options,
808
- );
809
- expect(result.isVariantOf?.model).toBe("0102769498");
810
- });
811
-
812
- it("keeps the SKU RefId reachable on the complete-variants shelf (dimension2)", () => {
813
- const result = toProductShelf(makeProduct(), makeSku(), 0, {
814
- ...options,
815
- shelfCompleteVariants: true,
816
- });
817
- const variantProps = result.isVariantOf?.hasVariant?.[0]?.additionalProperty ?? [];
818
- expect(variantProps).toEqual(
819
- expect.arrayContaining([expect.objectContaining({ name: "RefId", value: "REF-SKU1" })]),
820
- );
821
- });
822
-
823
- // A real catalog registers the packshot and the colour thumbnail LAST:
824
- // measured on a live store, `secondary` sits at index 6 and `color-thumbnail`
825
- // at index 7 of 8. A card that selects either BY NAME gets nothing from a
826
- // positional cap and silently falls back to the first photo (the model
827
- // wearing the piece).
828
- const labelledImages = [
829
- ...Array.from({ length: 6 }, (_, i) => ({
830
- imageUrl: `https://img.com/photo-${i}.jpg`,
831
- imageText: `Photo ${i}`,
832
- imageLabel: `product-photo-${i}`,
833
- })),
834
- { imageUrl: "https://img.com/still.jpg", imageText: "Still", imageLabel: "secondary" },
835
- { imageUrl: "https://img.com/thumb.jpg", imageText: "Thumb", imageLabel: "color-thumbnail" },
836
- ];
837
- const skuWithLabels = () => makeSku({ images: labelledImages });
838
- const labelledProduct = () => makeProduct({ items: [skuWithLabels()] });
839
-
840
- it("keeps the 2-image cap when keepImageNames is absent", () => {
841
- const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, options);
842
- expect(result.image?.map((i) => i.name)).toEqual(["product-photo-0", "product-photo-1"]);
843
- });
844
-
845
- it("keepImageNames adds back labelled images that fall outside the cap", () => {
846
- const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
847
- ...options,
848
- keepImageNames: ["secondary", "color-thumbnail"],
849
- });
850
- expect(result.image?.map((i) => i.name)).toEqual([
851
- "product-photo-0",
852
- "product-photo-1",
853
- "secondary",
854
- "color-thumbnail",
855
- ]);
856
- });
857
-
858
- it("keepImageNames does not duplicate a label already inside the cap", () => {
859
- const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
860
- ...options,
861
- keepImageNames: ["product-photo-0", "secondary"],
862
- });
863
- expect(result.image?.map((i) => i.name)).toEqual([
864
- "product-photo-0",
865
- "product-photo-1",
866
- "secondary",
867
- ]);
868
- });
869
-
870
- it("keepImageNames listing a label the SKU does not have changes nothing", () => {
871
- const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
872
- ...options,
873
- keepImageNames: ["vira"],
874
- });
875
- expect(result.image?.map((i) => i.name)).toEqual(["product-photo-0", "product-photo-1"]);
876
- });
877
-
878
- it("maxImages overrides the shelf default cap", () => {
879
- const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
880
- ...options,
881
- maxImages: 3,
882
- });
883
- expect(result.image).toHaveLength(3);
884
- });
885
- });
886
-
887
- // ---------------------------------------------------------------------------
888
- // toProduct — escape hatches for leanVariants listings
889
- // ---------------------------------------------------------------------------
890
- //
891
- // `leanVariants` assumes the card renders the ROOT sku, so `buildOfferVariant`
892
- // empties `priceSpecification` on every `hasVariant[]` entry. A card that picks
893
- // a representative variant instead (e.g. "cheapest in stock") reads that
894
- // entry's own offer for list price and installments — blank with the ladder
895
- // emptied. `displayedVariantId` keeps exactly that one entry full.
896
- //
897
- // `maxImages` caps `image[]` by POSITION. Both default to undefined, which
898
- // preserves the previous output byte for byte — that is what the last two cases
899
- // pin down.
900
-
901
- describe("toProduct — displayedVariantId / maxImages", () => {
902
- const sellers = (price: number) => [
903
- {
904
- sellerId: "1",
905
- sellerName: "Seller One",
906
- commertialOffer: {
907
- AvailableQuantity: 5,
908
- Price: price,
909
- ListPrice: price + 30,
910
- spotPrice: price,
911
- PriceValidUntil: "2025-12-31",
912
- // A real ladder: every payment method the store accepts, every
913
- // installment count. Measured on a live listing: 46 rungs per SKU,
914
- // 12.0 KB — the thing `leanOffer` exists to drop.
915
- Installments: ["Visa", "Master", "Amex", "PIX"].flatMap((method) =>
916
- Array.from({ length: 3 }, (_, i) => ({
917
- Value: price / (i + 1),
918
- NumberOfInstallments: i + 1,
919
- Name: `${method} ${i + 1}x`,
920
- InterestRate: 0,
921
- TotalValuePlusInterestRate: price,
922
- PaymentSystemName: method,
923
- })),
924
- ),
925
- GiftSkuIds: [],
926
- teasers: [],
927
- },
928
- },
929
- ];
930
-
931
- const makeSku = (itemId: string, price: number, imageCount = 4) =>
932
- ({
933
- itemId,
934
- name: `SKU ${itemId}`,
935
- ean: "1234567890123",
936
- referenceId: [{ Key: "RefId", Value: `REF-${itemId}` }],
937
- images: Array.from({ length: imageCount }, (_, i) => ({
938
- imageUrl: `https://img.com/${itemId}-${i}.jpg`,
939
- imageText: `img${i}`,
940
- imageLabel: `label${i}`,
941
- })),
942
- videos: [],
943
- sellers: sellers(price),
944
- variations: [{ name: "Cor", values: ["Preto"] }],
945
- kitItems: [],
946
- complementName: "",
947
- estimatedDateArrival: null,
948
- modalType: null,
949
- }) as any;
950
-
951
- const makeProduct = (items: any[]) =>
952
- ({
953
- origin: "intelligent-search",
954
- productId: "PROD1",
955
- productName: "Test Product",
956
- brand: "TestBrand",
957
- brandId: 1,
958
- brandImageUrl: null,
959
- productReference: "REF1",
960
- description: "desc",
961
- releaseDate: "2024-01-01",
962
- linkText: "test-product",
963
- categories: ["/Electronics/"],
964
- categoriesIds: ["/1/"],
965
- categoryId: "1",
966
- productClusters: [],
967
- clusterHighlights: [],
968
- items,
969
- }) as any;
970
-
971
- const options = { baseUrl: "https://example.com", priceCurrency: "BRL" };
972
- const items = [makeSku("SKU1", 90), makeSku("SKU2", 60), makeSku("SKU3", 120)];
973
- const product = makeProduct(items);
974
-
975
- const ladderOf = (variant: any) => variant?.offers?.offers?.[0]?.priceSpecification ?? [];
976
-
977
- /** Regra do caller: só os tipos base, nenhuma parcela. */
978
- const onlyListAndSale = (specs: any[]) =>
979
- specs.filter((s) => !s.priceComponentType);
980
-
981
- it("leanVariants alone empties the payment ladder on every variant", () => {
982
- const result = toProduct(product, items[0], 0, { ...options, leanVariants: true });
983
- const variants = result.isVariantOf?.hasVariant ?? [];
984
- expect(variants).toHaveLength(3);
985
- for (const v of variants) expect(ladderOf(v)).toEqual([]);
986
- });
987
-
988
- it("displayedVariantId keeps the ladder on the ONE variant the card renders", () => {
989
- const result = toProduct(product, items[0], 0, {
990
- ...options,
991
- leanVariants: true,
992
- displayedVariantId: (skus) => skus.find((s: any) => s.itemId === "SKU2")?.itemId,
993
- });
994
- const variants = result.isVariantOf?.hasVariant ?? [];
995
- const kept = variants.find((v: any) => v.sku === "SKU2");
996
- const lean = variants.filter((v: any) => v.sku !== "SKU2");
997
-
998
- expect(ladderOf(kept).length).toBeGreaterThan(0);
999
- for (const v of lean) expect(ladderOf(v)).toEqual([]);
1000
- });
1001
-
1002
- it("the kept variant stays LEAN — only its offer is upgraded", () => {
1003
- const result = toProduct(product, items[0], 0, {
1004
- ...options,
1005
- leanVariants: true,
1006
- displayedVariantId: () => "SKU2",
1007
- });
1008
- const kept: any = (result.isVariantOf?.hasVariant ?? []).find((v: any) => v.sku === "SKU2");
1009
-
1010
- // A full toProduct here would re-emit these; the lean shape must not.
1011
- expect(kept.description).toBeUndefined();
1012
- expect(kept.brand).toBeUndefined();
1013
- expect(kept.gtin).toBeUndefined();
1014
- expect(kept.isVariantOf).toBeUndefined();
1015
- // ...but the ladder is real. Without a `priceSpecifications` rule it is
1016
- // the FULL one — dropping description/brand/isVariantOf is the win here,
1017
- // and the ladder stays whatever the caller asked for.
1018
- const full = ladderOf(toProduct(product, items[1], 0, options));
1019
- expect(ladderOf(kept).length).toBe(full.length);
1020
- });
1021
-
1022
- it("the kept variant keeps its real inventoryLevel (selectors read it per SKU)", () => {
1023
- const result = toProduct(product, items[0], 0, {
1024
- ...options,
1025
- leanVariants: true,
1026
- displayedVariantId: () => "SKU2",
1027
- });
1028
- const kept: any = (result.isVariantOf?.hasVariant ?? []).find((v: any) => v.sku === "SKU2");
1029
- // buildOfferShelf hard-zeroes inventoryLevel; the variant path must not
1030
- // inherit that, or every variant reads as out of stock.
1031
- expect(kept.offers?.offers?.[0]?.inventoryLevel?.value).toBe(5);
1032
- });
1033
-
1034
- it("the ladder flag does NOT leak into the lean variants", () => {
1035
- // Regression: the internal `variantKeepLadder` reached `variantOptions`,
1036
- // so every lean variant emitted a ladder instead of an empty one and the
1037
- // option made the payload BIGGER — measured on a real listing,
1038
- // isVariantOf 38.6 -> 49.6 KB.
1039
- const result = toProduct(product, items[0], 0, {
1040
- ...options,
1041
- leanVariants: true,
1042
- priceSpecifications: onlyListAndSale,
1043
- displayedVariantId: () => "SKU2",
1044
- });
1045
- const variants = result.isVariantOf?.hasVariant ?? [];
1046
- for (const v of variants.filter((v: any) => v.sku !== "SKU2")) {
1047
- expect(ladderOf(v)).toEqual([]);
1048
- }
1049
- expect(ladderOf(variants.find((v: any) => v.sku === "SKU2")).length).toBeGreaterThan(0);
1050
- });
1051
-
1052
- it("priceSpecifications rewrites the ROOT ladder with the caller's own rule", () => {
1053
- const full = toProduct(product, items[0], 0, options);
1054
- const lean = toProduct(product, items[0], 0, {
1055
- ...options,
1056
- priceSpecifications: onlyListAndSale,
1057
- });
1058
-
1059
- expect(ladderOf(full).length).toBeGreaterThan(ladderOf(lean).length);
1060
- const types = new Set(ladderOf(lean).map((s: any) => s.priceType));
1061
- expect(types.has("https://schema.org/ListPrice")).toBe(true);
1062
- expect(types.has("https://schema.org/SalePrice")).toBe(true);
1063
- });
1064
-
1065
- it("displayedVariantId returning undefined leaves every variant lean", () => {
1066
- const result = toProduct(product, items[0], 0, {
1067
- ...options,
1068
- leanVariants: true,
1069
- displayedVariantId: () => undefined,
1070
- });
1071
- for (const v of result.isVariantOf?.hasVariant ?? []) expect(ladderOf(v)).toEqual([]);
1072
- });
1073
-
1074
- it("maxImages caps image[] by position", () => {
1075
- const full = toProduct(product, items[0], 0, options);
1076
- expect(full.image).toHaveLength(4);
1077
-
1078
- const capped = toProduct(product, items[0], 0, { ...options, maxImages: 2 });
1079
- expect(capped.image).toHaveLength(2);
1080
- expect(capped.image?.map((i) => i.url)).toEqual(
1081
- full.image?.slice(0, 2).map((i) => i.url),
1082
- );
1083
- });
1084
-
1085
- it("keepImageNames rescues a labelled image from the maxImages cap", () => {
1086
- const capped = toProduct(product, items[0], 0, {
1087
- ...options,
1088
- maxImages: 2,
1089
- keepImageNames: ["label3"],
1090
- });
1091
- expect(capped.image?.map((i) => i.name)).toEqual(["label0", "label1", "label3"]);
1092
- });
1093
-
1094
- it("keepImageNames without maxImages is a no-op — every image is already there", () => {
1095
- const before = toProduct(product, items[0], 0, options);
1096
- const after = toProduct(product, items[0], 0, { ...options, keepImageNames: ["label3"] });
1097
- expect(JSON.stringify(after)).toBe(JSON.stringify(before));
1098
- });
1099
-
1100
- it("every option absent is byte-for-byte the previous output", () => {
1101
- const before = toProduct(product, items[0], 0, options);
1102
- const after = toProduct(product, items[0], 0, {
1103
- ...options,
1104
- displayedVariantId: undefined,
1105
- maxImages: undefined,
1106
- keepImageNames: undefined,
1107
- priceSpecifications: undefined,
1108
- });
1109
- expect(JSON.stringify(after)).toBe(JSON.stringify(before));
1110
- });
1111
- });