@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.
- package/package.json +7 -52
- package/src/index.ts +10 -28
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +75 -65
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +354 -343
- package/src/README.md +0 -6
- package/src/__conformance__/upstream.test.ts +0 -158
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -724
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -56
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
- package/src/loaders/intelligentSearch/productList.ts +0 -170
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
- package/src/loaders/intelligentSearch/suggestions.ts +0 -44
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
- package/src/loaders/legacy.ts +0 -710
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -157
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -62
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.cacheHeaders.test.ts +0 -44
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -173
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
- package/src/utils/__tests__/transform.test.ts +0 -1111
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/buildOfferShelf.test.ts +0 -59
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -556
- package/src/utils/fetch.ts +0 -111
- package/src/utils/fetchCache.ts +0 -55
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -98
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -495
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -35
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -283
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1691
- package/src/utils/vtexId.ts +0 -151
- package/src/vtexClient.test.ts +0 -204
- 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
|
-
});
|