@decocms/apps-vtex 8.0.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 +12 -15
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +76 -72
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +444 -0
- package/src/README.md +0 -6
- 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 -667
- 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/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -58
- 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 -113
- package/src/loaders/intelligentSearch/productList.ts +0 -154
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -526
- package/src/loaders/intelligentSearch/suggestions.ts +0 -46
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -81
- package/src/loaders/legacy.ts +0 -635
- 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 -159
- 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 -64
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -157
- 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__/transform.test.ts +0 -769
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -560
- package/src/utils/fetch.ts +0 -107
- package/src/utils/fetchCache.ts +0 -199
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -97
- 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 -435
- 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 -37
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -282
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1522
- package/src/utils/vtexId.ts +0 -138
- package/tsconfig.json +0 -7
package/src/commerceLoaders.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standard VTEX commerce loader map factory for CMS block resolution.
|
|
3
|
-
*
|
|
4
|
-
* Wraps all VTEX loaders with createCachedLoader, applies universal
|
|
5
|
-
* workarounds (slug fallback, IS sort sanitization, map=productClusterIds),
|
|
6
|
-
* and registers both `.ts` and `.ts`-less aliases.
|
|
7
|
-
*
|
|
8
|
-
* Sites call `createVtexCommerceLoaders()` instead of manually wiring ~30
|
|
9
|
-
* individual loader entries. Site-specific loaders are merged via `extra`.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createCachedLoader } from "@decocms/blocks/sdk/cachedLoader";
|
|
13
|
-
import type { CacheProfileName } from "@decocms/blocks/sdk/cacheHeaders";
|
|
14
|
-
import { getCategoryTree } from "./loaders/catalog";
|
|
15
|
-
import vtexProductDetailsPage from "./loaders/intelligentSearch/productDetailsPage";
|
|
16
|
-
import vtexProductListShelf from "./loaders/intelligentSearch/productList";
|
|
17
|
-
import vtexProductListingPage from "./loaders/intelligentSearch/productListingPage";
|
|
18
|
-
import vtexSuggestions from "./loaders/intelligentSearch/suggestions";
|
|
19
|
-
import vtexRelatedProducts from "./loaders/legacy/relatedProductsLoader";
|
|
20
|
-
import vtexProductList from "./loaders/productListFull";
|
|
21
|
-
import vtexWorkflowProducts from "./loaders/workflow/products";
|
|
22
|
-
import { registerVtexSchemas } from "./schemas";
|
|
23
|
-
import { VALID_IS_SORTS } from "./utils/intelligentSearch";
|
|
24
|
-
|
|
25
|
-
export type CommerceLoaderFn = (props: any) => Promise<any>;
|
|
26
|
-
|
|
27
|
-
export interface VtexCommerceLoadersOptions {
|
|
28
|
-
/** Override cache profiles per loader type. */
|
|
29
|
-
cacheProfiles?: {
|
|
30
|
-
listing?: CacheProfileName;
|
|
31
|
-
product?: CacheProfileName;
|
|
32
|
-
search?: CacheProfileName;
|
|
33
|
-
static?: CacheProfileName;
|
|
34
|
-
};
|
|
35
|
-
/** Additional loaders to merge into the map (site-specific). */
|
|
36
|
-
extra?: Record<string, CommerceLoaderFn>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Bridge __pagePath → slug when CMS doesn't set slug explicitly.
|
|
41
|
-
* VTEX PDP pages receive __pagePath (e.g. "/produto-slug/p") but the
|
|
42
|
-
* inline loader only reads the `slug` field.
|
|
43
|
-
*/
|
|
44
|
-
function pdpWithSlugFallback(props: any): Promise<any> {
|
|
45
|
-
if ((!props.slug || props.slug.length === 0) && props.__pagePath) {
|
|
46
|
-
props = { ...props, slug: props.__pagePath };
|
|
47
|
-
}
|
|
48
|
-
return vtexProductDetailsPage(props);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Extract collection name from PLP product data.
|
|
53
|
-
* Products carry cluster info in additionalProperty with name="cluster".
|
|
54
|
-
*/
|
|
55
|
-
function extractCollectionName(result: any, collectionId: string): string | null {
|
|
56
|
-
if (!result?.products?.length) return null;
|
|
57
|
-
for (const product of result.products) {
|
|
58
|
-
const props = product.additionalProperty || product.isVariantOf?.additionalProperty || [];
|
|
59
|
-
for (const prop of props) {
|
|
60
|
-
if (prop.name === "cluster" && prop.propertyID === collectionId) {
|
|
61
|
-
return prop.value || null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Returns the standard VTEX commerce loader map for CMS resolution.
|
|
70
|
-
*
|
|
71
|
-
* Includes all Intelligent Search, legacy, category tree, and navbar loaders
|
|
72
|
-
* with SWR caching. Also registers `.ts`-less aliases for invoke compatibility.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```ts
|
|
76
|
-
* import { createVtexCommerceLoaders } from "@decocms/apps/vtex/commerceLoaders";
|
|
77
|
-
*
|
|
78
|
-
* const COMMERCE_LOADERS = {
|
|
79
|
-
* ...createVtexCommerceLoaders(),
|
|
80
|
-
* // site-specific only:
|
|
81
|
-
* "site/loaders/myCustomLoader": async (props) => { ... },
|
|
82
|
-
* };
|
|
83
|
-
* registerCommerceLoaders(COMMERCE_LOADERS);
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export function createVtexCommerceLoaders(
|
|
87
|
-
options?: VtexCommerceLoadersOptions,
|
|
88
|
-
): Record<string, CommerceLoaderFn> {
|
|
89
|
-
// Real props schemas for the admin meta. The stub registration that
|
|
90
|
-
// registerCommerceLoaders() later performs for this returned map never
|
|
91
|
-
// overwrites these (real-over-stub precedence in the schema registry).
|
|
92
|
-
registerVtexSchemas();
|
|
93
|
-
|
|
94
|
-
const profiles = {
|
|
95
|
-
listing: options?.cacheProfiles?.listing ?? "listing",
|
|
96
|
-
product: options?.cacheProfiles?.product ?? "product",
|
|
97
|
-
search: options?.cacheProfiles?.search ?? "search",
|
|
98
|
-
static: options?.cacheProfiles?.static ?? "static",
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const _cachedProductList = createCachedLoader(
|
|
102
|
-
"vtex/productList",
|
|
103
|
-
vtexProductList,
|
|
104
|
-
profiles.listing,
|
|
105
|
-
);
|
|
106
|
-
const cachedProductListShelf = createCachedLoader(
|
|
107
|
-
"vtex/productListShelf",
|
|
108
|
-
vtexProductListShelf,
|
|
109
|
-
profiles.listing,
|
|
110
|
-
);
|
|
111
|
-
const cachedPDP = createCachedLoader(
|
|
112
|
-
"vtex/productDetailsPage",
|
|
113
|
-
pdpWithSlugFallback,
|
|
114
|
-
profiles.product,
|
|
115
|
-
);
|
|
116
|
-
const _cachedPLP = createCachedLoader(
|
|
117
|
-
"vtex/productListingPage",
|
|
118
|
-
vtexProductListingPage,
|
|
119
|
-
profiles.listing,
|
|
120
|
-
);
|
|
121
|
-
const cachedSuggestions = createCachedLoader(
|
|
122
|
-
"vtex/suggestions",
|
|
123
|
-
vtexSuggestions,
|
|
124
|
-
profiles.search,
|
|
125
|
-
);
|
|
126
|
-
const cachedRelatedProducts = createCachedLoader(
|
|
127
|
-
"vtex/relatedProducts",
|
|
128
|
-
vtexRelatedProducts,
|
|
129
|
-
profiles.product,
|
|
130
|
-
);
|
|
131
|
-
const cachedWorkflowProducts = createCachedLoader(
|
|
132
|
-
"vtex/workflowProducts",
|
|
133
|
-
vtexWorkflowProducts,
|
|
134
|
-
profiles.listing,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* PLP wrapper: handles `map=productClusterIds` legacy URLs and sanitizes
|
|
139
|
-
* IS sort parameters that would cause VTEX API 400 errors.
|
|
140
|
-
*/
|
|
141
|
-
const cachedPLP: CommerceLoaderFn = async (props) => {
|
|
142
|
-
if (props.__pageUrl && !props.selectedFacets?.length) {
|
|
143
|
-
try {
|
|
144
|
-
const pageUrl = new URL(props.__pageUrl, "https://localhost");
|
|
145
|
-
const mapParam = pageUrl.searchParams.get("map");
|
|
146
|
-
if (mapParam && props.__pagePath) {
|
|
147
|
-
const segments = props.__pagePath.split("/").filter(Boolean);
|
|
148
|
-
const mapValues = mapParam.split(",");
|
|
149
|
-
const facets: Array<{ key: string; value: string }> = [];
|
|
150
|
-
for (let i = 0; i < Math.min(segments.length, mapValues.length); i++) {
|
|
151
|
-
const key = mapValues[i].trim();
|
|
152
|
-
const value = decodeURIComponent(segments[i]);
|
|
153
|
-
if (key && value) facets.push({ key, value });
|
|
154
|
-
}
|
|
155
|
-
if (facets.length) {
|
|
156
|
-
const rawSort = pageUrl.searchParams.get("sort") ?? "";
|
|
157
|
-
const cleanSort = VALID_IS_SORTS.has(rawSort) ? rawSort : "";
|
|
158
|
-
|
|
159
|
-
if (rawSort !== cleanSort) {
|
|
160
|
-
if (cleanSort) {
|
|
161
|
-
pageUrl.searchParams.set("sort", cleanSort);
|
|
162
|
-
} else {
|
|
163
|
-
pageUrl.searchParams.delete("sort");
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const result = await _cachedPLP({
|
|
168
|
-
...props,
|
|
169
|
-
selectedFacets: facets,
|
|
170
|
-
sort: cleanSort || undefined,
|
|
171
|
-
__pageUrl: pageUrl.toString(),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const clusterFacet = facets.find((f) => f.key === "productClusterIds");
|
|
175
|
-
if (result && clusterFacet) {
|
|
176
|
-
const collectionName = extractCollectionName(result, clusterFacet.value);
|
|
177
|
-
if (collectionName) {
|
|
178
|
-
result.breadcrumb = {
|
|
179
|
-
"@type": "BreadcrumbList",
|
|
180
|
-
itemListElement: [
|
|
181
|
-
{
|
|
182
|
-
"@type": "ListItem",
|
|
183
|
-
name: collectionName,
|
|
184
|
-
item: props.__pagePath || "/",
|
|
185
|
-
position: 1,
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
numberOfItems: 1,
|
|
189
|
-
};
|
|
190
|
-
result.seo = { ...result.seo, title: collectionName };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (e) {
|
|
197
|
-
console.error("[PLP] Error parsing map param:", e);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return _cachedPLP(props);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Related products wrapper: extracts slug from __pagePath when the CMS
|
|
205
|
-
* requestToParam stub returns null (standard in TanStack sites).
|
|
206
|
-
*/
|
|
207
|
-
const relatedWithSlugFallback: CommerceLoaderFn = (props) => {
|
|
208
|
-
if (!props.slug && props.__pagePath) {
|
|
209
|
-
const path = props.__pagePath.replace(/\/p$/, "").replace(/^\//, "");
|
|
210
|
-
props = { ...props, slug: path };
|
|
211
|
-
}
|
|
212
|
-
return cachedRelatedProducts(props);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const loaders: Record<string, CommerceLoaderFn> = {
|
|
216
|
-
// Intelligent Search loaders
|
|
217
|
-
"vtex/loaders/intelligentSearch/productListingPage.ts": cachedPLP,
|
|
218
|
-
"vtex/loaders/intelligentSearch/productList.ts": cachedProductListShelf,
|
|
219
|
-
"vtex/loaders/intelligentSearch/productDetailsPage.ts": cachedPDP,
|
|
220
|
-
"vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions,
|
|
221
|
-
// Legacy loaders (map to same cached functions)
|
|
222
|
-
"vtex/loaders/legacy/productDetailsPage.ts": cachedPDP,
|
|
223
|
-
"vtex/loaders/legacy/productList.ts": cachedProductListShelf,
|
|
224
|
-
"vtex/loaders/legacy/relatedProductsLoader.ts": relatedWithSlugFallback,
|
|
225
|
-
// Workflow
|
|
226
|
-
"vtex/loaders/workflow/products.ts": cachedWorkflowProducts,
|
|
227
|
-
// Top-level aliases (used by some CMS block configs)
|
|
228
|
-
"vtex/loaders/ProductList.ts": cachedProductListShelf,
|
|
229
|
-
"vtex/loaders/ProductDetailsPage.ts": cachedPDP,
|
|
230
|
-
"vtex/loaders/ProductListingPage.ts": cachedPLP,
|
|
231
|
-
// Category tree
|
|
232
|
-
"vtex/loaders/categories/tree": (props: any) => getCategoryTree(props?.categoryLevels ?? 3),
|
|
233
|
-
// Commerce passthrough loaders
|
|
234
|
-
"commerce/loaders/navbar.ts": async (props: any) => props.items ?? [],
|
|
235
|
-
"commerce/loaders/product/extensions/detailsPage.ts": async (props: any) => {
|
|
236
|
-
const data = props.data;
|
|
237
|
-
if (data?.product) return data;
|
|
238
|
-
return cachedPDP({ __pagePath: props.__pagePath });
|
|
239
|
-
},
|
|
240
|
-
// requestToParam stub — unresolvable in TanStack, pdpWithSlugFallback bridges it
|
|
241
|
-
"website/functions/requestToParam.ts": async () => null,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// Register .ts-less aliases for invoke compatibility
|
|
245
|
-
const withAliases: Record<string, CommerceLoaderFn> = { ...loaders };
|
|
246
|
-
for (const key of Object.keys(loaders)) {
|
|
247
|
-
if (key.endsWith(".ts")) {
|
|
248
|
-
withAliases[key.slice(0, -3)] = loaders[key];
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (options?.extra) {
|
|
253
|
-
Object.assign(withAliases, options.extra);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return withAliases;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Exposes the cached PDP loader for site-specific section loaders that need
|
|
261
|
-
* to call it directly (e.g. ProductDescription fallback).
|
|
262
|
-
*
|
|
263
|
-
* Returns a new instance each call — sites should cache the reference.
|
|
264
|
-
*/
|
|
265
|
-
export function createCachedPDPLoader(profile: CacheProfileName = "product"): CommerceLoaderFn {
|
|
266
|
-
return createCachedLoader("vtex/productDetailsPage", pdpWithSlugFallback, profile);
|
|
267
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* Render-based tests for the `createCart` factory (Cart v2).
|
|
4
|
-
*
|
|
5
|
-
* The factory-shape tests in `createCart.test.ts` run in node and never render.
|
|
6
|
-
* These exercise the actual hook behaviour that needs a React renderer:
|
|
7
|
-
* `useAddToCart().add()` returning the server's projected payload (so a caller
|
|
8
|
-
* can drive a toast without a second fetch) and the optimistic badge bump.
|
|
9
|
-
*
|
|
10
|
-
* No `@testing-library/react` in the tree — we hand-roll a tiny render harness
|
|
11
|
-
* on `react-dom/client` (jsdom env, declared in the docblock above).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { CartSummaryWithItems } from "@decocms/apps-commerce/types";
|
|
15
|
-
import { act, createElement } from "react";
|
|
16
|
-
import { createRoot } from "react-dom/client";
|
|
17
|
-
import { describe, expect, it } from "vitest";
|
|
18
|
-
import { type CreateCartInvoke, createCart } from "../createCart";
|
|
19
|
-
|
|
20
|
-
// React's act() requires this flag when driving updates manually.
|
|
21
|
-
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
-
|
|
23
|
-
function renderHook<T>(useHook: () => T) {
|
|
24
|
-
const ref = { current: undefined as unknown as T };
|
|
25
|
-
function Probe() {
|
|
26
|
-
ref.current = useHook();
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const container = document.createElement("div");
|
|
30
|
-
const root = createRoot(container);
|
|
31
|
-
act(() => {
|
|
32
|
-
root.render(createElement(Probe));
|
|
33
|
-
});
|
|
34
|
-
return { ref, unmount: () => act(() => root.unmount()) };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Fake invoke whose addItemsToCartV2 returns a `summary+items` projection. */
|
|
38
|
-
function makeInvoke(): CreateCartInvoke {
|
|
39
|
-
const projected: CartSummaryWithItems = {
|
|
40
|
-
orderFormId: "of-1",
|
|
41
|
-
totalItems: 1,
|
|
42
|
-
total: 100,
|
|
43
|
-
items: [
|
|
44
|
-
{
|
|
45
|
-
item_id: "sku-1",
|
|
46
|
-
item_name: "Camiseta",
|
|
47
|
-
item_variant: "P Azul",
|
|
48
|
-
image: "https://img.example/x.jpg",
|
|
49
|
-
price: 100,
|
|
50
|
-
quantity: 1,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
vtex: {
|
|
56
|
-
actions: {
|
|
57
|
-
getOrCreateCartV2: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
58
|
-
addItemsToCartV2: async () => projected as never,
|
|
59
|
-
updateCartItemsV2: async () => ({ ok: true }) as never,
|
|
60
|
-
addCouponToCartV2: async () => ({ ok: true }) as never,
|
|
61
|
-
},
|
|
62
|
-
loaders: {
|
|
63
|
-
cart: {
|
|
64
|
-
summary: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
65
|
-
full: async () => ({ original: null }) as never,
|
|
66
|
-
shipping: async () => ({ postalCode: "00000-000", options: [] }),
|
|
67
|
-
gifts: async () => ({ orderFormId: "of-1", selectableGifts: [], ratesAndBenefits: null }),
|
|
68
|
-
attachments: async () => ({
|
|
69
|
-
orderFormId: "of-1",
|
|
70
|
-
itemIndex: 0,
|
|
71
|
-
attachments: [],
|
|
72
|
-
attachmentOfferings: [],
|
|
73
|
-
}),
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe("createCart — useAddToCart.add()", () => {
|
|
81
|
-
it("returns the projected payload for a toast without a second fetch", async () => {
|
|
82
|
-
const cart = createCart({ invoke: makeInvoke() });
|
|
83
|
-
const { ref, unmount } = renderHook(() => cart.useAddToCart());
|
|
84
|
-
|
|
85
|
-
let result: unknown;
|
|
86
|
-
await act(async () => {
|
|
87
|
-
result = await ref.current.add({ id: "sku-1", seller: "1" });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
expect(result).toMatchObject({
|
|
91
|
-
totalItems: 1,
|
|
92
|
-
total: 100,
|
|
93
|
-
items: [{ item_name: "Camiseta", item_variant: "P Azul", price: 100 }],
|
|
94
|
-
});
|
|
95
|
-
unmount();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("reconciles the summary badge from the projected response", async () => {
|
|
99
|
-
const cart = createCart({ invoke: makeInvoke() });
|
|
100
|
-
const add = renderHook(() => cart.useAddToCart());
|
|
101
|
-
const badge = renderHook(() => cart.useCartSummary());
|
|
102
|
-
|
|
103
|
-
expect(badge.ref.current.totalItems).toBe(0);
|
|
104
|
-
|
|
105
|
-
await act(async () => {
|
|
106
|
-
await add.ref.current.add({ id: "sku-1", seller: "1" });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Reconciled from the server projection (1), not just the optimistic bump.
|
|
110
|
-
expect(badge.ref.current.totalItems).toBe(1);
|
|
111
|
-
add.unmount();
|
|
112
|
-
badge.unmount();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the `createCart` factory (Cart v2).
|
|
3
|
-
*
|
|
4
|
-
* Like `createUseCart`, hook render semantics need a React renderer that
|
|
5
|
-
* apps-start does not pull in, so we cover the factory's shape and instance
|
|
6
|
-
* isolation. The optimistic + reconciliation behaviour is exercised by the
|
|
7
|
-
* site-level smoke test.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, expect, it, vi } from "vitest";
|
|
11
|
-
import { type CreateCartInvoke, createCart } from "../createCart";
|
|
12
|
-
|
|
13
|
-
function makeInvoke(): CreateCartInvoke {
|
|
14
|
-
const summary = async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 });
|
|
15
|
-
const anyNoop = async () => ({ ok: true }) as never;
|
|
16
|
-
return {
|
|
17
|
-
vtex: {
|
|
18
|
-
actions: {
|
|
19
|
-
getOrCreateCartV2: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
20
|
-
addItemsToCartV2: anyNoop,
|
|
21
|
-
updateCartItemsV2: anyNoop,
|
|
22
|
-
addCouponToCartV2: anyNoop,
|
|
23
|
-
},
|
|
24
|
-
loaders: {
|
|
25
|
-
cart: {
|
|
26
|
-
summary,
|
|
27
|
-
full: async () => ({
|
|
28
|
-
original: null,
|
|
29
|
-
storefront: {
|
|
30
|
-
items: [],
|
|
31
|
-
total: 0,
|
|
32
|
-
subtotal: 0,
|
|
33
|
-
discounts: 0,
|
|
34
|
-
locale: "pt-BR",
|
|
35
|
-
currency: "BRL",
|
|
36
|
-
freeShippingTarget: 0,
|
|
37
|
-
checkoutHref: "/checkout",
|
|
38
|
-
},
|
|
39
|
-
}),
|
|
40
|
-
shipping: async () => ({ postalCode: "00000-000", options: [] }),
|
|
41
|
-
gifts: async () => ({ orderFormId: "of-1", selectableGifts: [], ratesAndBenefits: null }),
|
|
42
|
-
attachments: async () => ({
|
|
43
|
-
orderFormId: "of-1",
|
|
44
|
-
itemIndex: 0,
|
|
45
|
-
attachments: [],
|
|
46
|
-
attachmentOfferings: [],
|
|
47
|
-
}),
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("createCart — factory shape", () => {
|
|
55
|
-
it("returns the full granular hook set", () => {
|
|
56
|
-
const cart = createCart({ invoke: makeInvoke() });
|
|
57
|
-
expect(typeof cart.useCart).toBe("function");
|
|
58
|
-
expect(typeof cart.useCartSummary).toBe("function");
|
|
59
|
-
expect(typeof cart.useAddToCart).toBe("function");
|
|
60
|
-
expect(typeof cart.useShipping).toBe("function");
|
|
61
|
-
expect(typeof cart.useGifts).toBe("function");
|
|
62
|
-
expect(typeof cart.useAttachments).toBe("function");
|
|
63
|
-
expect(typeof cart.resetCart).toBe("function");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("two factory calls produce independent hook identities", () => {
|
|
67
|
-
const a = createCart({ invoke: makeInvoke() });
|
|
68
|
-
const b = createCart({ invoke: makeInvoke() });
|
|
69
|
-
expect(a.useCart).not.toBe(b.useCart);
|
|
70
|
-
expect(a.useAddToCart).not.toBe(b.useAddToCart);
|
|
71
|
-
expect(a.resetCart).not.toBe(b.resetCart);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("accepts custom cookie name / max-age without throwing", () => {
|
|
75
|
-
expect(() =>
|
|
76
|
-
createCart({
|
|
77
|
-
invoke: makeInvoke(),
|
|
78
|
-
orderFormCookieName: "custom_of",
|
|
79
|
-
orderFormCookieMaxAge: 3600,
|
|
80
|
-
}),
|
|
81
|
-
).not.toThrow();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("does not call any invoke method at construction time (lazy)", () => {
|
|
85
|
-
const invoke = makeInvoke();
|
|
86
|
-
const getOrCreate = vi.spyOn(invoke.vtex.actions, "getOrCreateCartV2");
|
|
87
|
-
const summary = vi.spyOn(invoke.vtex.loaders.cart, "summary");
|
|
88
|
-
createCart({ invoke });
|
|
89
|
-
expect(getOrCreate).not.toHaveBeenCalled();
|
|
90
|
-
expect(summary).not.toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the `createUseCart` factory.
|
|
3
|
-
*
|
|
4
|
-
* The hook itself depends on React (useState/useEffect), and apps-start
|
|
5
|
-
* does not pull in @testing-library/react. So we test the parts of the
|
|
6
|
-
* factory that don't require a React renderer: shape, isolation, and the
|
|
7
|
-
* pure `itemToAnalyticsItem` helper. Hook semantics are exercised by the
|
|
8
|
-
* site-level integration smoke test (the template has shipped to two
|
|
9
|
-
* production sites).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, expect, it } from "vitest";
|
|
13
|
-
import { type CreateUseCartInvoke, createUseCart } from "../createUseCart";
|
|
14
|
-
|
|
15
|
-
function makeInvoke(): CreateUseCartInvoke {
|
|
16
|
-
const noop = async () => ({ orderFormId: "of-1", items: [] }) as never;
|
|
17
|
-
return {
|
|
18
|
-
vtex: {
|
|
19
|
-
actions: {
|
|
20
|
-
getOrCreateCart: noop,
|
|
21
|
-
addItemsToCart: noop,
|
|
22
|
-
updateCartItems: noop,
|
|
23
|
-
addCouponToCart: noop,
|
|
24
|
-
updateOrderFormAttachment: noop,
|
|
25
|
-
simulateCart: noop,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("createUseCart — factory shape", () => {
|
|
32
|
-
it("returns useCart, resetCart, itemToAnalyticsItem", () => {
|
|
33
|
-
const cart = createUseCart({ invoke: makeInvoke() });
|
|
34
|
-
expect(typeof cart.useCart).toBe("function");
|
|
35
|
-
expect(typeof cart.resetCart).toBe("function");
|
|
36
|
-
expect(typeof cart.itemToAnalyticsItem).toBe("function");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("two factory calls produce independent itemToAnalyticsItem references", () => {
|
|
40
|
-
const a = createUseCart({ invoke: makeInvoke() });
|
|
41
|
-
const b = createUseCart({ invoke: makeInvoke() });
|
|
42
|
-
// Different closures => different function identities.
|
|
43
|
-
expect(a.itemToAnalyticsItem).not.toBe(b.itemToAnalyticsItem);
|
|
44
|
-
expect(a.useCart).not.toBe(b.useCart);
|
|
45
|
-
expect(a.resetCart).not.toBe(b.resetCart);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("accepts custom orderFormCookieName / maxAge without throwing", () => {
|
|
49
|
-
expect(() =>
|
|
50
|
-
createUseCart({
|
|
51
|
-
invoke: makeInvoke(),
|
|
52
|
-
orderFormCookieName: "custom_of_id",
|
|
53
|
-
orderFormCookieMaxAge: 3600,
|
|
54
|
-
}),
|
|
55
|
-
).not.toThrow();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("createUseCart — itemToAnalyticsItem", () => {
|
|
60
|
-
const { itemToAnalyticsItem } = createUseCart({ invoke: makeInvoke() });
|
|
61
|
-
|
|
62
|
-
it("maps the canonical fields", () => {
|
|
63
|
-
const out = itemToAnalyticsItem(
|
|
64
|
-
{
|
|
65
|
-
id: "sku-1",
|
|
66
|
-
productId: "prod-1",
|
|
67
|
-
name: "Widget",
|
|
68
|
-
skuName: "Widget Blue",
|
|
69
|
-
sellingPrice: 1990,
|
|
70
|
-
price: 2490,
|
|
71
|
-
listPrice: 2490,
|
|
72
|
-
quantity: 2,
|
|
73
|
-
seller: "1",
|
|
74
|
-
additionalInfo: { brandName: "Acme" },
|
|
75
|
-
} as never,
|
|
76
|
-
0,
|
|
77
|
-
);
|
|
78
|
-
expect(out).toEqual({
|
|
79
|
-
item_id: "prod-1",
|
|
80
|
-
item_group_id: "prod-1",
|
|
81
|
-
item_name: "Widget",
|
|
82
|
-
item_variant: "Widget Blue",
|
|
83
|
-
item_brand: "Acme",
|
|
84
|
-
price: 19.9,
|
|
85
|
-
discount: 5,
|
|
86
|
-
quantity: 2,
|
|
87
|
-
coupon: undefined,
|
|
88
|
-
affiliation: "1",
|
|
89
|
-
index: 0,
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("falls back to skuName when name is missing", () => {
|
|
94
|
-
const out = itemToAnalyticsItem(
|
|
95
|
-
{
|
|
96
|
-
productId: "p",
|
|
97
|
-
skuName: "Fallback",
|
|
98
|
-
sellingPrice: 100,
|
|
99
|
-
listPrice: 100,
|
|
100
|
-
quantity: 1,
|
|
101
|
-
seller: "1",
|
|
102
|
-
} as never,
|
|
103
|
-
3,
|
|
104
|
-
);
|
|
105
|
-
expect(out.item_name).toBe("Fallback");
|
|
106
|
-
expect(out.index).toBe(3);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("uses price when sellingPrice is missing", () => {
|
|
110
|
-
const out = itemToAnalyticsItem(
|
|
111
|
-
{
|
|
112
|
-
productId: "p",
|
|
113
|
-
name: "n",
|
|
114
|
-
price: 1000,
|
|
115
|
-
listPrice: 1000,
|
|
116
|
-
quantity: 1,
|
|
117
|
-
seller: "1",
|
|
118
|
-
} as never,
|
|
119
|
-
0,
|
|
120
|
-
);
|
|
121
|
-
expect(out.price).toBe(10);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("computes zero discount when listPrice equals sellingPrice", () => {
|
|
125
|
-
const out = itemToAnalyticsItem(
|
|
126
|
-
{
|
|
127
|
-
productId: "p",
|
|
128
|
-
name: "n",
|
|
129
|
-
sellingPrice: 500,
|
|
130
|
-
listPrice: 500,
|
|
131
|
-
quantity: 1,
|
|
132
|
-
seller: "1",
|
|
133
|
-
} as never,
|
|
134
|
-
0,
|
|
135
|
-
);
|
|
136
|
-
expect(out.discount).toBe(0);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("rounds discount to 2 decimal places", () => {
|
|
140
|
-
const out = itemToAnalyticsItem(
|
|
141
|
-
{
|
|
142
|
-
productId: "p",
|
|
143
|
-
name: "n",
|
|
144
|
-
sellingPrice: 1000,
|
|
145
|
-
listPrice: 1333,
|
|
146
|
-
quantity: 1,
|
|
147
|
-
seller: "1",
|
|
148
|
-
} as never,
|
|
149
|
-
0,
|
|
150
|
-
);
|
|
151
|
-
expect(out.discount).toBe(3.33);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("preserves coupon when supplied", () => {
|
|
155
|
-
const out = itemToAnalyticsItem(
|
|
156
|
-
{
|
|
157
|
-
productId: "p",
|
|
158
|
-
name: "n",
|
|
159
|
-
sellingPrice: 100,
|
|
160
|
-
listPrice: 100,
|
|
161
|
-
quantity: 1,
|
|
162
|
-
seller: "1",
|
|
163
|
-
coupon: "SAVE10",
|
|
164
|
-
} as never,
|
|
165
|
-
0,
|
|
166
|
-
);
|
|
167
|
-
expect(out.coupon).toBe("SAVE10");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("uses empty string brand when additionalInfo is missing", () => {
|
|
171
|
-
const out = itemToAnalyticsItem(
|
|
172
|
-
{
|
|
173
|
-
productId: "p",
|
|
174
|
-
name: "n",
|
|
175
|
-
sellingPrice: 100,
|
|
176
|
-
listPrice: 100,
|
|
177
|
-
quantity: 1,
|
|
178
|
-
seller: "1",
|
|
179
|
-
} as never,
|
|
180
|
-
0,
|
|
181
|
-
);
|
|
182
|
-
expect(out.item_brand).toBe("");
|
|
183
|
-
});
|
|
184
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the `createUseUser` factory.
|
|
3
|
-
*
|
|
4
|
-
* Same approach as `createUseCart.test.ts`: apps-start does not pull
|
|
5
|
-
* @testing-library/react, so we test factory shape and isolation. Hook
|
|
6
|
-
* semantics are covered by site-level smoke tests.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, expect, it } from "vitest";
|
|
10
|
-
import { type CreateUseUserInvoke, createUseUser } from "../createUseUser";
|
|
11
|
-
|
|
12
|
-
function makeInvoke(): CreateUseUserInvoke {
|
|
13
|
-
return {
|
|
14
|
-
vtex: {
|
|
15
|
-
loaders: {
|
|
16
|
-
user: async () => null,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("createUseUser — factory shape", () => {
|
|
23
|
-
it("returns useUser, resetUser", () => {
|
|
24
|
-
const u = createUseUser({ invoke: makeInvoke() });
|
|
25
|
-
expect(typeof u.useUser).toBe("function");
|
|
26
|
-
expect(typeof u.resetUser).toBe("function");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("two factory calls produce independent function references", () => {
|
|
30
|
-
const a = createUseUser({ invoke: makeInvoke() });
|
|
31
|
-
const b = createUseUser({ invoke: makeInvoke() });
|
|
32
|
-
expect(a.useUser).not.toBe(b.useUser);
|
|
33
|
-
expect(a.resetUser).not.toBe(b.resetUser);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("does not throw at construction time even with a failing invoke", () => {
|
|
37
|
-
const invoke: CreateUseUserInvoke = {
|
|
38
|
-
vtex: {
|
|
39
|
-
loaders: {
|
|
40
|
-
user: async () => {
|
|
41
|
-
throw new Error("boom");
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
expect(() => createUseUser({ invoke })).not.toThrow();
|
|
47
|
-
});
|
|
48
|
-
});
|