@decocms/apps-vtex 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +67 -0
- package/src/README.md +6 -0
- package/src/__tests__/client-segment-cookie.test.ts +255 -0
- package/src/__tests__/client-set-cookie-forward.test.ts +257 -0
- package/src/actions/address.ts +250 -0
- package/src/actions/analytics/sendEvent.ts +86 -0
- package/src/actions/auth.ts +333 -0
- package/src/actions/checkout.ts +522 -0
- package/src/actions/index.ts +11 -0
- package/src/actions/masterData.ts +168 -0
- package/src/actions/misc.ts +188 -0
- package/src/actions/newsletter.ts +105 -0
- package/src/actions/orders.ts +34 -0
- package/src/actions/profile.ts +201 -0
- package/src/actions/session.ts +85 -0
- package/src/actions/trigger.ts +43 -0
- package/src/actions/wishlist.ts +114 -0
- package/src/client.ts +667 -0
- package/src/commerceLoaders.ts +261 -0
- package/src/hooks/__tests__/createUseCart.test.ts +184 -0
- package/src/hooks/__tests__/createUseUser.test.ts +48 -0
- package/src/hooks/__tests__/createUseWishlist.test.ts +87 -0
- package/src/hooks/createUseCart.ts +360 -0
- package/src/hooks/createUseUser.ts +153 -0
- package/src/hooks/createUseWishlist.ts +242 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useAutocomplete.ts +83 -0
- package/src/hooks/useCart.ts +243 -0
- package/src/hooks/useUser.ts +78 -0
- package/src/hooks/useWishlist.ts +119 -0
- package/src/index.ts +17 -0
- package/src/invoke.ts +181 -0
- package/src/loaders/ProductDetailsPage.ts +1 -0
- package/src/loaders/ProductList.ts +1 -0
- package/src/loaders/ProductListingPage.ts +1 -0
- package/src/loaders/address.ts +115 -0
- package/src/loaders/autocomplete.ts +58 -0
- package/src/loaders/brands.ts +44 -0
- package/src/loaders/cart.ts +52 -0
- package/src/loaders/catalog.ts +163 -0
- package/src/loaders/collections.ts +55 -0
- package/src/loaders/index.ts +19 -0
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +20 -0
- package/src/loaders/intelligentSearch/productDetailsPage.ts +95 -0
- package/src/loaders/intelligentSearch/productList.ts +154 -0
- package/src/loaders/intelligentSearch/productListingPage.ts +506 -0
- package/src/loaders/intelligentSearch/suggestions.ts +46 -0
- package/src/loaders/legacy/productDetailsPage.ts +1 -0
- package/src/loaders/legacy/productList.ts +1 -0
- package/src/loaders/legacy/relatedProductsLoader.ts +81 -0
- package/src/loaders/legacy.ts +635 -0
- package/src/loaders/logistics.ts +111 -0
- package/src/loaders/minicart.ts +78 -0
- package/src/loaders/navbar.ts +26 -0
- package/src/loaders/orders.ts +92 -0
- package/src/loaders/pageType.ts +68 -0
- package/src/loaders/payment.ts +97 -0
- package/src/loaders/productListFull.ts +159 -0
- package/src/loaders/profile.ts +138 -0
- package/src/loaders/promotion.ts +30 -0
- package/src/loaders/search.ts +124 -0
- package/src/loaders/session.ts +83 -0
- package/src/loaders/user.ts +87 -0
- package/src/loaders/wishlist.ts +89 -0
- package/src/loaders/wishlistProducts.ts +69 -0
- package/src/loaders/workflow/products.ts +64 -0
- package/src/loaders/workflow.ts +316 -0
- package/src/logo.png +0 -0
- package/src/middleware.ts +240 -0
- package/src/mod.ts +152 -0
- package/src/registry.ts +9 -0
- package/src/types.ts +248 -0
- package/src/utils/__tests__/cookieSanitizer.test.ts +162 -0
- package/src/utils/__tests__/fetch.test.ts +80 -0
- package/src/utils/__tests__/fetchCache.test.ts +112 -0
- package/src/utils/__tests__/instrumentedFetch.test.ts +158 -0
- package/src/utils/__tests__/intelligentSearch.test.ts +88 -0
- package/src/utils/__tests__/minicart.test.ts +184 -0
- package/src/utils/__tests__/operationRouter.test.ts +227 -0
- package/src/utils/__tests__/resourceRange.test.ts +32 -0
- package/src/utils/__tests__/sitemap.test.ts +185 -0
- package/src/utils/__tests__/slugify.test.ts +31 -0
- package/src/utils/__tests__/transform.test.ts +698 -0
- package/src/utils/accountLoaders.ts +203 -0
- package/src/utils/authHelpers.ts +85 -0
- package/src/utils/batch.ts +18 -0
- package/src/utils/cookieSanitizer.ts +173 -0
- package/src/utils/cookies.ts +167 -0
- package/src/utils/enrichment.ts +560 -0
- package/src/utils/fetch.ts +107 -0
- package/src/utils/fetchCache.ts +222 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/instrumentedFetch.ts +78 -0
- package/src/utils/intelligentSearch.ts +96 -0
- package/src/utils/legacy.ts +139 -0
- package/src/utils/minicart.ts +139 -0
- package/src/utils/operationRouter.ts +139 -0
- package/src/utils/pickAndOmit.ts +25 -0
- package/src/utils/proxy.ts +435 -0
- package/src/utils/resourceRange.ts +10 -0
- package/src/utils/segment.ts +152 -0
- package/src/utils/similars.ts +37 -0
- package/src/utils/sitemap.ts +282 -0
- package/src/utils/slugCache.ts +28 -0
- package/src/utils/slugify.ts +13 -0
- package/src/utils/transform.ts +1509 -0
- package/src/utils/types.ts +1884 -0
- package/src/utils/vtexId.ts +138 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,698 @@
|
|
|
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
|
+
toProductVariant,
|
|
27
|
+
} from "../transform";
|
|
28
|
+
|
|
29
|
+
const makeOffer = (price: number, availability: string): Offer => ({
|
|
30
|
+
"@type": "Offer",
|
|
31
|
+
price,
|
|
32
|
+
availability: availability as Offer["availability"],
|
|
33
|
+
priceSpecification: [],
|
|
34
|
+
inventoryLevel: { value: 1 },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("inStock", () => {
|
|
38
|
+
it("returns true when in stock", () => {
|
|
39
|
+
expect(inStock(makeOffer(10, SCHEMA_IN_STOCK))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns false when out of stock", () => {
|
|
43
|
+
expect(inStock(makeOffer(10, SCHEMA_OUT_OF_STOCK))).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("bestOfferFirst", () => {
|
|
48
|
+
it("sorts in-stock before out-of-stock", () => {
|
|
49
|
+
const inStockOffer = makeOffer(100, SCHEMA_IN_STOCK);
|
|
50
|
+
const outOfStockOffer = makeOffer(10, SCHEMA_OUT_OF_STOCK);
|
|
51
|
+
expect(bestOfferFirst(inStockOffer, outOfStockOffer)).toBe(-1);
|
|
52
|
+
expect(bestOfferFirst(outOfStockOffer, inStockOffer)).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sorts by price when both in stock", () => {
|
|
56
|
+
const cheap = makeOffer(10, SCHEMA_IN_STOCK);
|
|
57
|
+
const expensive = makeOffer(100, SCHEMA_IN_STOCK);
|
|
58
|
+
expect(bestOfferFirst(cheap, expensive)).toBeLessThan(0);
|
|
59
|
+
expect(bestOfferFirst(expensive, cheap)).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns 0 for equal offers", () => {
|
|
63
|
+
const a = makeOffer(50, SCHEMA_IN_STOCK);
|
|
64
|
+
const b = makeOffer(50, SCHEMA_IN_STOCK);
|
|
65
|
+
expect(bestOfferFirst(a, b)).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("aggregateOffers", () => {
|
|
70
|
+
it("returns undefined for empty array", () => {
|
|
71
|
+
expect(aggregateOffers([])).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("aggregates single offer", () => {
|
|
75
|
+
const offer = makeOffer(100, SCHEMA_IN_STOCK);
|
|
76
|
+
const result = aggregateOffers([offer], "BRL");
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
"@type": "AggregateOffer",
|
|
79
|
+
priceCurrency: "BRL",
|
|
80
|
+
highPrice: 100,
|
|
81
|
+
lowPrice: 100,
|
|
82
|
+
offerCount: 1,
|
|
83
|
+
offers: [offer],
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("finds low and high prices across multiple offers", () => {
|
|
88
|
+
const offers = [
|
|
89
|
+
makeOffer(50, SCHEMA_IN_STOCK),
|
|
90
|
+
makeOffer(100, SCHEMA_IN_STOCK),
|
|
91
|
+
makeOffer(25, SCHEMA_IN_STOCK),
|
|
92
|
+
];
|
|
93
|
+
const result = aggregateOffers(offers, "BRL");
|
|
94
|
+
expect(result?.lowPrice).toBe(25);
|
|
95
|
+
expect(result?.highPrice).toBe(100);
|
|
96
|
+
expect(result?.offerCount).toBe(3);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("ignores out-of-stock offers for high price", () => {
|
|
100
|
+
const offers = [makeOffer(25, SCHEMA_IN_STOCK), makeOffer(200, SCHEMA_OUT_OF_STOCK)];
|
|
101
|
+
const result = aggregateOffers(offers, "BRL");
|
|
102
|
+
expect(result?.highPrice).toBe(25);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("pickSku", () => {
|
|
107
|
+
const makeProduct = (
|
|
108
|
+
items: Array<{
|
|
109
|
+
itemId: string;
|
|
110
|
+
sellers: Array<{ commertialOffer: { AvailableQuantity: number } }>;
|
|
111
|
+
}>,
|
|
112
|
+
) => ({ items, origin: "intelligent-search" }) as any;
|
|
113
|
+
|
|
114
|
+
it("returns specified SKU", () => {
|
|
115
|
+
const product = makeProduct([
|
|
116
|
+
{ itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
|
|
117
|
+
{ itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
|
|
118
|
+
]);
|
|
119
|
+
expect(pickSku(product, "2").itemId).toBe("2");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns first available SKU when no ID specified", () => {
|
|
123
|
+
const product = makeProduct([
|
|
124
|
+
{ itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
|
|
125
|
+
{ itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
|
|
126
|
+
]);
|
|
127
|
+
expect(pickSku(product).itemId).toBe("2");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("falls back to first SKU when none available", () => {
|
|
131
|
+
const product = makeProduct([
|
|
132
|
+
{ itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
|
|
133
|
+
{ itemId: "2", sellers: [{ commertialOffer: { AvailableQuantity: 0 } }] },
|
|
134
|
+
]);
|
|
135
|
+
expect(pickSku(product).itemId).toBe("1");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("falls back to first SKU when specified ID not found", () => {
|
|
139
|
+
const product = makeProduct([
|
|
140
|
+
{ itemId: "1", sellers: [{ commertialOffer: { AvailableQuantity: 5 } }] },
|
|
141
|
+
]);
|
|
142
|
+
expect(pickSku(product, "999").itemId).toBe("1");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("toAdditionalPropertyCategory", () => {
|
|
147
|
+
it("creates category property value", () => {
|
|
148
|
+
const result = toAdditionalPropertyCategory({ propertyID: "123", value: "Shoes" });
|
|
149
|
+
expect(result).toEqual({
|
|
150
|
+
"@type": "PropertyValue",
|
|
151
|
+
name: "category",
|
|
152
|
+
propertyID: "123",
|
|
153
|
+
value: "Shoes",
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("toAdditionalPropertyCluster", () => {
|
|
159
|
+
it("creates cluster property value", () => {
|
|
160
|
+
const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" });
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
"@type": "PropertyValue",
|
|
163
|
+
name: "cluster",
|
|
164
|
+
propertyID: "456",
|
|
165
|
+
value: "Sale",
|
|
166
|
+
description: undefined,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("marks as highlight when in set", () => {
|
|
171
|
+
const highlights = new Set(["456"]);
|
|
172
|
+
const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" }, highlights);
|
|
173
|
+
expect(result.description).toBe("highlight");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not mark as highlight when not in set", () => {
|
|
177
|
+
const highlights = new Set(["789"]);
|
|
178
|
+
const result = toAdditionalPropertyCluster({ propertyID: "456", value: "Sale" }, highlights);
|
|
179
|
+
expect(result.description).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("toAdditionalPropertyReferenceId", () => {
|
|
184
|
+
it("creates reference ID property value", () => {
|
|
185
|
+
const result = toAdditionalPropertyReferenceId({ name: "RefId", value: "ABC123" });
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
"@type": "PropertyValue",
|
|
188
|
+
name: "RefId",
|
|
189
|
+
value: "ABC123",
|
|
190
|
+
valueReference: "ReferenceID",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("toAdditionalPropertySpecification", () => {
|
|
196
|
+
it("creates specification property value", () => {
|
|
197
|
+
const result = toAdditionalPropertySpecification({ name: "Color", value: "Red" });
|
|
198
|
+
expect(result).toEqual({
|
|
199
|
+
"@type": "PropertyValue",
|
|
200
|
+
name: "Color",
|
|
201
|
+
value: "Red",
|
|
202
|
+
propertyID: undefined,
|
|
203
|
+
valueReference: "SPECIFICATION",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("includes propertyID when provided", () => {
|
|
208
|
+
const result = toAdditionalPropertySpecification({
|
|
209
|
+
name: "Color",
|
|
210
|
+
value: "Red",
|
|
211
|
+
propertyID: "group1",
|
|
212
|
+
});
|
|
213
|
+
expect(result.propertyID).toBe("group1");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("filtersToSearchParams", () => {
|
|
218
|
+
it("converts facets to search params", () => {
|
|
219
|
+
const facets = [
|
|
220
|
+
{ key: "category", value: "shoes" },
|
|
221
|
+
{ key: "brand", value: "nike" },
|
|
222
|
+
];
|
|
223
|
+
const params = filtersToSearchParams(facets);
|
|
224
|
+
expect(params.get("filter.category")).toBe("shoes");
|
|
225
|
+
expect(params.get("filter.brand")).toBe("nike");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("preserves existing params", () => {
|
|
229
|
+
const existing = new URLSearchParams("page=1");
|
|
230
|
+
const params = filtersToSearchParams([{ key: "brand", value: "nike" }], existing);
|
|
231
|
+
expect(params.get("page")).toBe("1");
|
|
232
|
+
expect(params.get("filter.brand")).toBe("nike");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("legacyFacetsNormalize", () => {
|
|
237
|
+
it("normalizes legacy price format", () => {
|
|
238
|
+
const result = legacyFacetsNormalize("priceFrom", "de-34,90-a-56,90");
|
|
239
|
+
expect(result).toEqual({ key: "price", value: "34.90:56.90" });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("maps legacy key names", () => {
|
|
243
|
+
const result = legacyFacetsNormalize("productClusterSearchableIds", "123");
|
|
244
|
+
expect(result).toEqual({ key: "productClusterIds", value: "123" });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("passes through unknown keys", () => {
|
|
248
|
+
const result = legacyFacetsNormalize("brand", "nike");
|
|
249
|
+
expect(result).toEqual({ key: "brand", value: "nike" });
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("legacyFacetsFromURL", () => {
|
|
254
|
+
it("extracts facets from URL with map param", () => {
|
|
255
|
+
const url = new URL("https://example.com/shoes/nike?map=c,brand");
|
|
256
|
+
const result = legacyFacetsFromURL(url);
|
|
257
|
+
expect(result).toEqual([
|
|
258
|
+
{ key: "c", value: "shoes" },
|
|
259
|
+
{ key: "brand", value: "nike" },
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("returns empty array when no map param", () => {
|
|
264
|
+
const url = new URL("https://example.com/shoes");
|
|
265
|
+
const result = legacyFacetsFromURL(url);
|
|
266
|
+
expect(result).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("handles mismatched lengths", () => {
|
|
270
|
+
const url = new URL("https://example.com/shoes?map=c,brand,extra");
|
|
271
|
+
const result = legacyFacetsFromURL(url);
|
|
272
|
+
expect(result).toHaveLength(1);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("filtersFromURL", () => {
|
|
277
|
+
it("extracts both legacy and filter params", () => {
|
|
278
|
+
const url = new URL("https://example.com/shoes?map=c&filter.brand=nike");
|
|
279
|
+
const result = filtersFromURL(url);
|
|
280
|
+
expect(result).toEqual([
|
|
281
|
+
{ key: "c", value: "shoes" },
|
|
282
|
+
{ key: "brand", value: "nike" },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("extracts only filter params when no map", () => {
|
|
287
|
+
const url = new URL("https://example.com/?filter.brand=nike&filter.category=shoes");
|
|
288
|
+
const result = filtersFromURL(url);
|
|
289
|
+
expect(result).toEqual([
|
|
290
|
+
{ key: "brand", value: "nike" },
|
|
291
|
+
{ key: "category", value: "shoes" },
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("mergeFacets", () => {
|
|
297
|
+
it("merges two facet arrays", () => {
|
|
298
|
+
const f1 = [{ key: "brand", value: "nike" }];
|
|
299
|
+
const f2 = [{ key: "category", value: "shoes" }];
|
|
300
|
+
const result = mergeFacets(f1, f2);
|
|
301
|
+
expect(result).toHaveLength(2);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("deduplicates facets", () => {
|
|
305
|
+
const f1 = [{ key: "brand", value: "nike" }];
|
|
306
|
+
const f2 = [{ key: "brand", value: "nike" }];
|
|
307
|
+
const result = mergeFacets(f1, f2);
|
|
308
|
+
expect(result).toHaveLength(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("keeps both when same key different value", () => {
|
|
312
|
+
const f1 = [{ key: "brand", value: "nike" }];
|
|
313
|
+
const f2 = [{ key: "brand", value: "adidas" }];
|
|
314
|
+
const result = mergeFacets(f1, f2);
|
|
315
|
+
expect(result).toHaveLength(2);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("categoryTreeToNavbar", () => {
|
|
320
|
+
it("transforms tree to navbar elements", () => {
|
|
321
|
+
const tree = [
|
|
322
|
+
{
|
|
323
|
+
id: 1,
|
|
324
|
+
name: "Shoes",
|
|
325
|
+
hasChildren: true,
|
|
326
|
+
url: "https://example.com/shoes",
|
|
327
|
+
children: [
|
|
328
|
+
{
|
|
329
|
+
id: 2,
|
|
330
|
+
name: "Running",
|
|
331
|
+
hasChildren: false,
|
|
332
|
+
url: "https://example.com/shoes/running",
|
|
333
|
+
children: [],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
const result = categoryTreeToNavbar(tree);
|
|
339
|
+
expect(result).toEqual([
|
|
340
|
+
{
|
|
341
|
+
"@type": "SiteNavigationElement",
|
|
342
|
+
url: "/shoes",
|
|
343
|
+
name: "Shoes",
|
|
344
|
+
children: [
|
|
345
|
+
{
|
|
346
|
+
"@type": "SiteNavigationElement",
|
|
347
|
+
url: "/shoes/running",
|
|
348
|
+
name: "Running",
|
|
349
|
+
children: [],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("returns empty array for empty tree", () => {
|
|
357
|
+
expect(categoryTreeToNavbar([])).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("toBrand", () => {
|
|
362
|
+
it("transforms VTEX brand", () => {
|
|
363
|
+
const brand = {
|
|
364
|
+
id: 1,
|
|
365
|
+
name: "Nike",
|
|
366
|
+
imageUrl: "/brands/nike.png",
|
|
367
|
+
metaTagDescription: "Nike brand",
|
|
368
|
+
};
|
|
369
|
+
const result = toBrand(brand as any, "https://example.com");
|
|
370
|
+
expect(result).toEqual({
|
|
371
|
+
"@type": "Brand",
|
|
372
|
+
"@id": "1",
|
|
373
|
+
name: "Nike",
|
|
374
|
+
logo: "https://example.com/brands/nike.png",
|
|
375
|
+
description: "Nike brand",
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("keeps absolute URLs as-is", () => {
|
|
380
|
+
const brand = {
|
|
381
|
+
id: 1,
|
|
382
|
+
name: "Nike",
|
|
383
|
+
imageUrl: "https://cdn.example.com/nike.png",
|
|
384
|
+
metaTagDescription: "",
|
|
385
|
+
};
|
|
386
|
+
const result = toBrand(brand as any, "https://example.com");
|
|
387
|
+
expect(result.logo).toBe("https://cdn.example.com/nike.png");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("normalizeFacet", () => {
|
|
392
|
+
it("sets Map to priceFrom and Value to Slug", () => {
|
|
393
|
+
const facet = { Map: "c", Value: "shoes", Slug: "de-10-a-50", Name: "Price", Quantity: 5 };
|
|
394
|
+
const result = normalizeFacet(facet as any);
|
|
395
|
+
expect(result.Map).toBe("priceFrom");
|
|
396
|
+
expect(result.Value).toBe("de-10-a-50");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("sortProducts", () => {
|
|
401
|
+
it("sorts products by specified order", () => {
|
|
402
|
+
const products = [
|
|
403
|
+
{ "@type": "Product", sku: "3" },
|
|
404
|
+
{ "@type": "Product", sku: "1" },
|
|
405
|
+
{ "@type": "Product", sku: "2" },
|
|
406
|
+
] as unknown as Product[];
|
|
407
|
+
const result = sortProducts(products, ["1", "2", "3"], "sku");
|
|
408
|
+
expect(result.map((p) => p.sku)).toEqual(["1", "2", "3"]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("returns undefined for missing IDs", () => {
|
|
412
|
+
const products = [{ "@type": "Product", sku: "1" }] as unknown as Product[];
|
|
413
|
+
const result = sortProducts(products, ["1", "999"], "sku");
|
|
414
|
+
expect(result[0].sku).toBe("1");
|
|
415
|
+
expect(result[1]).toBeUndefined();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("parsePageType", () => {
|
|
420
|
+
it("maps FullText to Search", () => {
|
|
421
|
+
expect(parsePageType({ pageType: "FullText" } as any)).toBe("Search");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("maps NotFound to Unknown", () => {
|
|
425
|
+
expect(parsePageType({ pageType: "NotFound" } as any)).toBe("Unknown");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("passes through other types", () => {
|
|
429
|
+
expect(parsePageType({ pageType: "Department" } as any)).toBe("Department");
|
|
430
|
+
expect(parsePageType({ pageType: "Brand" } as any)).toBe("Brand");
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("forceHttpsOnAssets", () => {
|
|
435
|
+
it("converts http to https on item images", () => {
|
|
436
|
+
const orderForm = {
|
|
437
|
+
items: [
|
|
438
|
+
{ imageUrl: "http://example.com/img.jpg" },
|
|
439
|
+
{ imageUrl: "https://example.com/img2.jpg" },
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
const result = forceHttpsOnAssets(orderForm as any);
|
|
443
|
+
expect(result.items[0].imageUrl).toBe("https://example.com/img.jpg");
|
|
444
|
+
expect(result.items[1].imageUrl).toBe("https://example.com/img2.jpg");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("handles items without imageUrl", () => {
|
|
448
|
+
const orderForm = { items: [{ imageUrl: undefined }] };
|
|
449
|
+
const result = forceHttpsOnAssets(orderForm as any);
|
|
450
|
+
expect(result.items[0].imageUrl).toBeUndefined();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe("toPostalAddress", () => {
|
|
455
|
+
it("transforms VTEX address to PostalAddress", () => {
|
|
456
|
+
const address = {
|
|
457
|
+
addressId: "addr1",
|
|
458
|
+
country: "BRA",
|
|
459
|
+
city: "São Paulo",
|
|
460
|
+
state: "SP",
|
|
461
|
+
neighborhood: "Pinheiros",
|
|
462
|
+
postalCode: "05422-000",
|
|
463
|
+
street: "Rua dos Pinheiros",
|
|
464
|
+
number: "123",
|
|
465
|
+
addressName: "Home",
|
|
466
|
+
receiverName: "John Doe",
|
|
467
|
+
complement: "Apt 1",
|
|
468
|
+
reference: "Near the park",
|
|
469
|
+
geoCoordinates: [-23.5668, -46.6901],
|
|
470
|
+
};
|
|
471
|
+
const result = toPostalAddress(address as any);
|
|
472
|
+
expect(result["@type"]).toBe("PostalAddress");
|
|
473
|
+
expect(result["@id"]).toBe("addr1");
|
|
474
|
+
expect(result.addressCountry).toBe("BRA");
|
|
475
|
+
expect(result.addressLocality).toBe("São Paulo");
|
|
476
|
+
expect(result.addressRegion).toBe("SP");
|
|
477
|
+
expect(result.postalCode).toBe("05422-000");
|
|
478
|
+
expect(result.streetAddress).toBe("Rua dos Pinheiros");
|
|
479
|
+
expect(result.identifier).toBe("123");
|
|
480
|
+
expect(result.name).toBe("Home");
|
|
481
|
+
expect(result.alternateName).toBe("John Doe");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("returns undefined for empty optional fields", () => {
|
|
485
|
+
const address = {
|
|
486
|
+
addressId: "addr1",
|
|
487
|
+
country: "BRA",
|
|
488
|
+
city: "SP",
|
|
489
|
+
state: "SP",
|
|
490
|
+
postalCode: "05422-000",
|
|
491
|
+
street: "Rua X",
|
|
492
|
+
};
|
|
493
|
+
const result = toPostalAddress(address as any);
|
|
494
|
+
expect(result.areaServed).toBeUndefined();
|
|
495
|
+
expect(result.identifier).toBeUndefined();
|
|
496
|
+
expect(result.name).toBeUndefined();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe("toProductVariant", () => {
|
|
501
|
+
const makeISProduct = (overrides: Record<string, unknown> = {}) =>
|
|
502
|
+
({
|
|
503
|
+
origin: "intelligent-search",
|
|
504
|
+
productId: "PROD1",
|
|
505
|
+
productName: "Test Product",
|
|
506
|
+
brand: "TestBrand",
|
|
507
|
+
brandId: 1,
|
|
508
|
+
brandImageUrl: null,
|
|
509
|
+
productReference: "REF1",
|
|
510
|
+
description: "Full description HTML",
|
|
511
|
+
releaseDate: "2024-01-01",
|
|
512
|
+
linkText: "test-product",
|
|
513
|
+
categories: ["/Electronics/TVs/"],
|
|
514
|
+
categoriesIds: ["/1/2/"],
|
|
515
|
+
categoryId: "2",
|
|
516
|
+
productClusters: { "100": "Sale" },
|
|
517
|
+
clusterHighlights: {},
|
|
518
|
+
items: [],
|
|
519
|
+
...overrides,
|
|
520
|
+
}) as any;
|
|
521
|
+
|
|
522
|
+
const makeISSku = (overrides: Record<string, unknown> = {}) =>
|
|
523
|
+
({
|
|
524
|
+
itemId: "SKU1",
|
|
525
|
+
name: "Test SKU",
|
|
526
|
+
ean: "1234567890123",
|
|
527
|
+
referenceId: [{ Key: "RefId", Value: "REF-SKU1" }],
|
|
528
|
+
images: [
|
|
529
|
+
{ imageUrl: "https://img.com/1.jpg", imageText: "Front", imageLabel: "front" },
|
|
530
|
+
{ imageUrl: "https://img.com/2.jpg", imageText: "Back", imageLabel: "back" },
|
|
531
|
+
{ imageUrl: "https://img.com/3.jpg", imageText: "Side", imageLabel: "side" },
|
|
532
|
+
],
|
|
533
|
+
videos: ["https://video.com/1.mp4"],
|
|
534
|
+
sellers: [
|
|
535
|
+
{
|
|
536
|
+
sellerId: "1",
|
|
537
|
+
sellerName: "Seller One",
|
|
538
|
+
commertialOffer: {
|
|
539
|
+
AvailableQuantity: 10,
|
|
540
|
+
Price: 99.9,
|
|
541
|
+
ListPrice: 129.9,
|
|
542
|
+
spotPrice: 89.9,
|
|
543
|
+
PriceValidUntil: "2025-12-31",
|
|
544
|
+
Installments: [
|
|
545
|
+
{
|
|
546
|
+
Value: 33.3,
|
|
547
|
+
NumberOfInstallments: 3,
|
|
548
|
+
Name: "Visa",
|
|
549
|
+
InterestRate: 0,
|
|
550
|
+
TotalValuePlusInterestRate: 99.9,
|
|
551
|
+
PaymentSystemName: "Visa",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
GiftSkuIds: [],
|
|
555
|
+
teasers: [],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
variations: [
|
|
560
|
+
{ name: "Cor", values: ["Preto"] },
|
|
561
|
+
{ name: "Voltagem", values: ["220V"] },
|
|
562
|
+
{ name: "Tamanho", values: ["G"] },
|
|
563
|
+
],
|
|
564
|
+
kitItems: [],
|
|
565
|
+
complementName: "Complement",
|
|
566
|
+
estimatedDateArrival: null,
|
|
567
|
+
modalType: null,
|
|
568
|
+
...overrides,
|
|
569
|
+
}) as any;
|
|
570
|
+
|
|
571
|
+
const baseOptions = {
|
|
572
|
+
baseUrl: "https://example.com",
|
|
573
|
+
priceCurrency: "BRL",
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
it("returns minimal product shape", () => {
|
|
577
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
578
|
+
const sku = makeISSku();
|
|
579
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
580
|
+
|
|
581
|
+
expect(result["@type"]).toBe("Product");
|
|
582
|
+
expect(result.productID).toBe("SKU1");
|
|
583
|
+
expect(result.sku).toBe("SKU1");
|
|
584
|
+
expect(result.name).toBe("Test SKU");
|
|
585
|
+
expect(result.url).toContain("/test-product/p");
|
|
586
|
+
expect(result.inProductGroupWithID).toBe("PROD1");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("drops description, video, brand, gtin, releaseDate, isVariantOf", () => {
|
|
590
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
591
|
+
const sku = makeISSku();
|
|
592
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
593
|
+
|
|
594
|
+
expect(result.video).toBeUndefined();
|
|
595
|
+
expect(result.description).toBeUndefined();
|
|
596
|
+
expect(result.brand).toBeUndefined();
|
|
597
|
+
expect(result.gtin).toBeUndefined();
|
|
598
|
+
expect(result.releaseDate).toBeUndefined();
|
|
599
|
+
expect(result.alternateName).toBeUndefined();
|
|
600
|
+
expect(result.isVariantOf).toBeUndefined();
|
|
601
|
+
expect(result.isAccessoryOrSparePartFor).toBeUndefined();
|
|
602
|
+
expect(result.category).toBeUndefined();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("includes image[0] by default — selectors render thumbnails from it", () => {
|
|
606
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
607
|
+
const sku = makeISSku();
|
|
608
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
609
|
+
|
|
610
|
+
expect(result.image).toHaveLength(1);
|
|
611
|
+
expect(result.image?.[0]).toMatchObject({
|
|
612
|
+
"@type": "ImageObject",
|
|
613
|
+
url: "https://img.com/1.jpg",
|
|
614
|
+
encodingFormat: "image",
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("includes real inventoryLevel by default — selectors gate stock state on it", () => {
|
|
619
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
620
|
+
const sku = makeISSku();
|
|
621
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
622
|
+
|
|
623
|
+
const offer = result.offers!.offers[0];
|
|
624
|
+
expect(offer.inventoryLevel?.value).toBe(10);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("drops image when variantIncludeImage: false", () => {
|
|
628
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
629
|
+
const sku = makeISSku();
|
|
630
|
+
const result = toProductVariant(product, sku, {
|
|
631
|
+
...baseOptions,
|
|
632
|
+
variantIncludeImage: false,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
expect(result.image).toBeUndefined();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("zeros inventoryLevel when variantIncludeInventory: false (legacy lean behavior)", () => {
|
|
639
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
640
|
+
const sku = makeISSku();
|
|
641
|
+
const result = toProductVariant(product, sku, {
|
|
642
|
+
...baseOptions,
|
|
643
|
+
variantIncludeInventory: false,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const offer = result.offers!.offers[0];
|
|
647
|
+
expect(offer.inventoryLevel?.value).toBe(0);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("filters additionalProperty to variant-differentiating names only", () => {
|
|
651
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
652
|
+
const sku = makeISSku();
|
|
653
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
654
|
+
|
|
655
|
+
const propNames = result.additionalProperty?.map((p) => p.name) ?? [];
|
|
656
|
+
// Should only contain Cor, Voltagem, Tamanho (from VARIANT_PROPERTY_NAMES)
|
|
657
|
+
for (const name of propNames) {
|
|
658
|
+
expect(["Cor", "Voltagem", "Tamanho"]).toContain(name);
|
|
659
|
+
}
|
|
660
|
+
expect(propNames.length).toBeGreaterThan(0);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("respects custom variantPropertyNames", () => {
|
|
664
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
665
|
+
const sku = makeISSku();
|
|
666
|
+
const result = toProductVariant(product, sku, {
|
|
667
|
+
...baseOptions,
|
|
668
|
+
variantPropertyNames: new Set(["Cor"]),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const propNames = result.additionalProperty?.map((p) => p.name) ?? [];
|
|
672
|
+
expect(propNames).toEqual(["Cor"]);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("produces lean offers with availability but no priceSpecification details", () => {
|
|
676
|
+
const product = makeISProduct({ items: [makeISSku()] });
|
|
677
|
+
const sku = makeISSku();
|
|
678
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
679
|
+
|
|
680
|
+
expect(result.offers).toBeDefined();
|
|
681
|
+
expect(result.offers?.offers).toHaveLength(1);
|
|
682
|
+
|
|
683
|
+
const offer = result.offers!.offers[0];
|
|
684
|
+
expect(offer.availability).toBe("https://schema.org/InStock");
|
|
685
|
+
expect(offer.seller).toBe("1");
|
|
686
|
+
expect(offer.priceSpecification).toEqual([]);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("handles SKU with no sellers", () => {
|
|
690
|
+
const product = makeISProduct({ items: [makeISSku({ sellers: [] })] });
|
|
691
|
+
const sku = makeISSku({ sellers: [] });
|
|
692
|
+
const result = toProductVariant(product, sku, baseOptions);
|
|
693
|
+
|
|
694
|
+
// Should still return a valid product, offers may be undefined (no sellers)
|
|
695
|
+
expect(result["@type"]).toBe("Product");
|
|
696
|
+
expect(result.productID).toBe("SKU1");
|
|
697
|
+
});
|
|
698
|
+
});
|