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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. package/tsconfig.json +0 -7
@@ -1,155 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { clearFetchCache, fetchWithCache, getFetchCacheStats } from "../fetchCache";
3
- import { VtexCircuitOpenError } from "../resilience";
4
-
5
- function mockResponse(body: unknown, status = 200): Response {
6
- return {
7
- ok: status >= 200 && status < 300,
8
- status,
9
- statusText: status === 200 ? "OK" : "Error",
10
- json: () => Promise.resolve(body),
11
- } as Response;
12
- }
13
-
14
- describe("fetchWithCache", () => {
15
- beforeEach(() => {
16
- clearFetchCache();
17
- });
18
-
19
- it("returns fetched data on cache miss", async () => {
20
- const data = { id: 1, name: "product" };
21
- const doFetch = vi.fn(() => Promise.resolve(mockResponse(data)));
22
-
23
- const result = await fetchWithCache("key1", doFetch);
24
- expect(result).toEqual(data);
25
- expect(doFetch).toHaveBeenCalledOnce();
26
- });
27
-
28
- it("returns cached data on subsequent calls", async () => {
29
- const data = { id: 1 };
30
- const doFetch = vi.fn(() => Promise.resolve(mockResponse(data)));
31
-
32
- await fetchWithCache("key2", doFetch);
33
- const result = await fetchWithCache("key2", doFetch);
34
-
35
- expect(result).toEqual(data);
36
- expect(doFetch).toHaveBeenCalledOnce();
37
- });
38
-
39
- it("deduplicates in-flight requests", async () => {
40
- const data = { id: 1 };
41
- const doFetch = vi.fn(
42
- () => new Promise<Response>((resolve) => setTimeout(() => resolve(mockResponse(data)), 10)),
43
- );
44
-
45
- const [r1, r2] = await Promise.all([
46
- fetchWithCache("key3", doFetch),
47
- fetchWithCache("key3", doFetch),
48
- ]);
49
-
50
- expect(r1).toEqual(data);
51
- expect(r2).toEqual(data);
52
- expect(doFetch).toHaveBeenCalledOnce();
53
- });
54
-
55
- it("throws on 5xx responses", async () => {
56
- const doFetch = vi.fn(() => Promise.resolve(mockResponse(null, 500)));
57
-
58
- await expect(fetchWithCache("key4", doFetch)).rejects.toThrow("500");
59
- });
60
-
61
- it("does not retry a circuit-open error (fast-fail)", async () => {
62
- const doFetch = vi.fn(() => Promise.reject(new VtexCircuitOpenError("host")));
63
-
64
- await expect(fetchWithCache("circuit-key", doFetch)).rejects.toBeInstanceOf(
65
- VtexCircuitOpenError,
66
- );
67
- // The breaker already decided to shed load — must not hammer with retries.
68
- expect(doFetch).toHaveBeenCalledOnce();
69
- });
70
-
71
- it("serves last-good stale data when a background refresh fails", async () => {
72
- vi.useFakeTimers();
73
- try {
74
- let call = 0;
75
- const doFetch = vi.fn(() => {
76
- call++;
77
- if (call === 1) return Promise.resolve(mockResponse({ v: "good" }));
78
- return Promise.reject(new VtexCircuitOpenError("host"));
79
- });
80
- // Prime a good entry.
81
- const first = await fetchWithCache<{ v: string }>("sie-key", doFetch);
82
- expect(first).toEqual({ v: "good" });
83
-
84
- // Advance past the 2xx freshness TTL (180s) but within the 24h SIE window.
85
- await vi.advanceTimersByTimeAsync(200_000);
86
-
87
- // Stale read: serves last-good immediately, refresh happens in background.
88
- const second = await fetchWithCache<{ v: string }>("sie-key", doFetch);
89
- expect(second).toEqual({ v: "good" });
90
- } finally {
91
- vi.useRealTimers();
92
- }
93
- });
94
-
95
- it("does not retry — retries are owned by the resilience layer", async () => {
96
- // A 5xx used to be retried 3× here, which also re-entered the circuit
97
- // breaker 3× per request. fetchCache now makes a single attempt.
98
- const doFetch = vi.fn(() => Promise.resolve(mockResponse(null, 500)));
99
- await expect(fetchWithCache("single-attempt", doFetch)).rejects.toThrow("500");
100
- expect(doFetch).toHaveBeenCalledOnce();
101
- });
102
-
103
- it("returns null for 404 responses", async () => {
104
- const doFetch = vi.fn(() => Promise.resolve(mockResponse(null, 404)));
105
-
106
- const result = await fetchWithCache("key5", doFetch);
107
- expect(result).toBeNull();
108
- });
109
-
110
- it("tracks cache stats", async () => {
111
- const doFetch = () => Promise.resolve(mockResponse({ ok: true }));
112
-
113
- expect(getFetchCacheStats()).toEqual({ entries: 0, inflight: 0 });
114
-
115
- await fetchWithCache("stats1", doFetch);
116
- await fetchWithCache("stats2", doFetch);
117
-
118
- expect(getFetchCacheStats()).toEqual({ entries: 2, inflight: 0 });
119
- });
120
-
121
- it("clears cache", async () => {
122
- const doFetch = () => Promise.resolve(mockResponse({ ok: true }));
123
- await fetchWithCache("clear1", doFetch);
124
-
125
- clearFetchCache();
126
- expect(getFetchCacheStats()).toEqual({ entries: 0, inflight: 0 });
127
- });
128
-
129
- it("evicts the inflight slot when the fetch never settles", async () => {
130
- vi.useFakeTimers();
131
- try {
132
- // `doFetch` returns a Promise that never resolves — simulates a hung
133
- // VTEX subrequest (TCP open, no FIN, no response). Without the
134
- // timeout guard, the inflight Map entry would leak forever and
135
- // subsequent callers would `await` a zombie Promise — the prod
136
- // memory-leak this fix addresses.
137
- const doFetch = vi.fn(() => new Promise<Response>(() => {}));
138
-
139
- const pending = fetchWithCache("hung-key", doFetch);
140
- // Swallow the eventual rejection so the unhandled rejection doesn't
141
- // fail the test runner.
142
- pending.catch(() => {});
143
-
144
- expect(getFetchCacheStats().inflight).toBe(1);
145
-
146
- // Fast-forward past the 15s inflight backstop timeout.
147
- await vi.advanceTimersByTimeAsync(16_000);
148
-
149
- await expect(pending).rejects.toThrow(/timed out/);
150
- expect(getFetchCacheStats().inflight).toBe(0);
151
- } finally {
152
- vi.useRealTimers();
153
- }
154
- });
155
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * Smoke tests for the pre-wired VTEX fetch factory.
3
- *
4
- * The deep coverage of `createInstrumentedFetch` lives in @decocms/start;
5
- * here we only verify the apps-start wiring decisions:
6
- *
7
- * - The URL router is plumbed through so unannotated callsites get
8
- * semantic span operations + histogram labels.
9
- * - The canonical `http.client.request.duration` histogram is recorded
10
- * with the right labels on every call (via the framework's
11
- * `recordCommerceMetric` helper).
12
- * - `disableHistogram: true` opts out cleanly.
13
- * - A caller's explicit `init.operation` wins over the URL router
14
- * (delegating to the framework, but worth asserting at this seam).
15
- */
16
-
17
- import { configureMeter, type MeterAdapter } from "@decocms/blocks/sdk/observability";
18
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
- import { createVtexFetch } from "../instrumentedFetch";
20
-
21
- type Labels = Record<string, string | number | boolean>;
22
- type HistogramCall = {
23
- name: string;
24
- value: number;
25
- attrs: Labels;
26
- };
27
-
28
- function captureHistogram(): { calls: HistogramCall[]; meter: MeterAdapter } {
29
- const calls: HistogramCall[] = [];
30
- const meter: MeterAdapter = {
31
- counterInc: vi.fn(),
32
- gaugeSet: vi.fn(),
33
- histogramRecord: (name, value, attrs) => {
34
- calls.push({ name, value, attrs: attrs ?? {} });
35
- },
36
- };
37
- return { calls, meter };
38
- }
39
-
40
- function mockOkResponse(status = 200): Response {
41
- return new Response(JSON.stringify({}), { status });
42
- }
43
-
44
- describe("createVtexFetch", () => {
45
- afterEach(() => {
46
- vi.restoreAllMocks();
47
- configureMeter({
48
- counterInc: () => {},
49
- gaugeSet: () => {},
50
- histogramRecord: () => {},
51
- });
52
- });
53
-
54
- it("records http.client.request.duration with provider/operation/status labels on success", async () => {
55
- const { calls, meter } = captureHistogram();
56
- configureMeter(meter);
57
-
58
- const baseFetch = vi.fn(async () => mockOkResponse(200));
59
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as typeof fetch });
60
-
61
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions");
62
-
63
- expect(calls).toHaveLength(1);
64
- expect(calls[0].name).toBe("http.client.request.duration");
65
- expect(calls[0].attrs).toMatchObject({
66
- provider: "vtex",
67
- operation: "sessions.get",
68
- status_class: "2xx",
69
- cached: false,
70
- });
71
- expect(calls[0].value).toBeGreaterThanOrEqual(0);
72
- });
73
-
74
- it("uses the URL router for unannotated calls", async () => {
75
- const { calls, meter } = captureHistogram();
76
- configureMeter(meter);
77
-
78
- const baseFetch = vi.fn(async () => mockOkResponse(200));
79
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as typeof fetch });
80
-
81
- await fetchFn(
82
- "https://store.vtexcommercestable.com.br/api/io/_v/api/intelligent-search/product_search/foo",
83
- );
84
-
85
- expect(calls[0].attrs.operation).toBe("intelligent-search.product_search");
86
- });
87
-
88
- it("honors init.operation over the URL router", async () => {
89
- const { calls, meter } = captureHistogram();
90
- configureMeter(meter);
91
-
92
- const baseFetch = vi.fn(async () => mockOkResponse(200));
93
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as typeof fetch });
94
-
95
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions", {
96
- operation: "explicit.custom_op",
97
- });
98
-
99
- expect(calls[0].attrs.operation).toBe("explicit.custom_op");
100
- });
101
-
102
- it("records cached=true when the response carries x-cache: HIT", async () => {
103
- const { calls, meter } = captureHistogram();
104
- configureMeter(meter);
105
-
106
- const baseFetch = vi.fn(
107
- async () => new Response("{}", { status: 200, headers: { "x-cache": "HIT" } }),
108
- );
109
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as typeof fetch });
110
-
111
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions");
112
-
113
- expect(calls[0].attrs.cached).toBe(true);
114
- });
115
-
116
- it("emits status_class derived from the actual response status", async () => {
117
- const { calls, meter } = captureHistogram();
118
- configureMeter(meter);
119
-
120
- const baseFetch = vi.fn(async () => mockOkResponse(503));
121
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as typeof fetch });
122
-
123
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions");
124
-
125
- expect(calls[0].attrs.status_class).toBe("5xx");
126
- });
127
-
128
- it("skips histogram emission when disableHistogram is true", async () => {
129
- const { calls, meter } = captureHistogram();
130
- configureMeter(meter);
131
-
132
- const baseFetch = vi.fn(async () => mockOkResponse(200));
133
- const fetchFn = createVtexFetch({
134
- baseFetch: baseFetch as typeof fetch,
135
- disableHistogram: true,
136
- });
137
-
138
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions");
139
-
140
- expect(calls).toHaveLength(0);
141
- });
142
-
143
- it("does not surface the operation field to the underlying fetch", async () => {
144
- const { meter } = captureHistogram();
145
- configureMeter(meter);
146
-
147
- const baseFetch = vi.fn(async (_input: unknown, _init?: RequestInit) => mockOkResponse(200));
148
- const fetchFn = createVtexFetch({ baseFetch: baseFetch as unknown as typeof fetch });
149
-
150
- await fetchFn("https://store.vtexcommercestable.com.br/api/sessions", {
151
- operation: "explicit.op",
152
- });
153
-
154
- expect(baseFetch).toHaveBeenCalledOnce();
155
- const init = baseFetch.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
156
- expect(init?.operation).toBeUndefined();
157
- });
158
- });
@@ -1,88 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isFilterParam, toPath, withDefaultFacets, withDefaultParams } from "../intelligentSearch";
3
-
4
- describe("withDefaultFacets", () => {
5
- it("returns a copy of the facets array", () => {
6
- const facets = [{ key: "category", value: "shoes" }];
7
- const result = withDefaultFacets(facets);
8
- expect(result).toEqual(facets);
9
- expect(result).not.toBe(facets);
10
- });
11
-
12
- it("returns empty array for empty input", () => {
13
- expect(withDefaultFacets([])).toEqual([]);
14
- });
15
- });
16
-
17
- describe("toPath", () => {
18
- it("builds path from facets", () => {
19
- const facets = [
20
- { key: "category", value: "shoes" },
21
- { key: "brand", value: "nike" },
22
- ];
23
- expect(toPath(facets)).toBe("category/shoes/brand/nike");
24
- });
25
-
26
- it("handles facets with empty key", () => {
27
- const facets = [{ key: "", value: "shoes" }];
28
- expect(toPath(facets)).toBe("shoes");
29
- });
30
-
31
- it("returns empty string for empty facets", () => {
32
- expect(toPath([])).toBe("");
33
- });
34
- });
35
-
36
- describe("withDefaultParams", () => {
37
- it("fills in defaults", () => {
38
- const result = withDefaultParams({});
39
- expect(result).toEqual({
40
- page: 1,
41
- count: 12,
42
- query: "",
43
- sort: "",
44
- fuzzy: "auto",
45
- locale: "pt-BR",
46
- hideUnavailableItems: false,
47
- simulationBehavior: "default",
48
- });
49
- });
50
-
51
- it("increments page by 1", () => {
52
- expect(withDefaultParams({ page: 0 }).page).toBe(1);
53
- expect(withDefaultParams({ page: 2 }).page).toBe(3);
54
- });
55
-
56
- it("preserves provided values", () => {
57
- const result = withDefaultParams({
58
- query: "shoes",
59
- count: 24,
60
- sort: "price:asc",
61
- fuzzy: "0",
62
- hideUnavailableItems: true,
63
- });
64
- expect(result.query).toBe("shoes");
65
- expect(result.count).toBe(24);
66
- expect(result.sort).toBe("price:asc");
67
- expect(result.fuzzy).toBe("0");
68
- expect(result.hideUnavailableItems).toBe(true);
69
- });
70
-
71
- it("omits fuzzy when empty string", () => {
72
- const result = withDefaultParams({ fuzzy: "" });
73
- expect(result).not.toHaveProperty("fuzzy");
74
- });
75
- });
76
-
77
- describe("isFilterParam", () => {
78
- it("returns true for filter params", () => {
79
- expect(isFilterParam("filter.category")).toBe(true);
80
- expect(isFilterParam("filter.brand")).toBe(true);
81
- });
82
-
83
- it("returns false for non-filter params", () => {
84
- expect(isFilterParam("page")).toBe(false);
85
- expect(isFilterParam("query")).toBe(false);
86
- expect(isFilterParam("filterNot")).toBe(false);
87
- });
88
- });
@@ -1,184 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { OrderForm, OrderFormItem, Totalizer } from "../../types";
3
- import { vtexOrderFormToMinicart } from "../minicart";
4
-
5
- const baseStorePreferences = {
6
- countryCode: "BRA",
7
- currencyCode: "BRL",
8
- currencyLocale: 1046,
9
- currencySymbol: "R$",
10
- saveUserData: true,
11
- timeZone: "E. South America Standard Time",
12
- currencyFormatInfo: {
13
- currencyDecimalDigits: 2,
14
- currencyDecimalSeparator: ",",
15
- currencyGroupSeparator: ".",
16
- currencyGroupSize: 3,
17
- startsWithCurrencySymbol: true,
18
- },
19
- };
20
-
21
- function makeOrderForm(overrides: Partial<OrderForm> = {}): OrderForm {
22
- return {
23
- orderFormId: "of-1",
24
- salesChannel: "1",
25
- loggedIn: false,
26
- isCheckedIn: false,
27
- allowManualPrice: false,
28
- canEditData: false,
29
- ignoreProfileData: false,
30
- value: 0,
31
- messages: [],
32
- items: [],
33
- totalizers: [],
34
- shippingData: null,
35
- clientProfileData: null,
36
- paymentData: null,
37
- marketingData: null,
38
- sellers: [],
39
- clientPreferencesData: { locale: "pt-BR" },
40
- storePreferencesData: baseStorePreferences,
41
- ...overrides,
42
- };
43
- }
44
-
45
- function makeItem(overrides: Partial<OrderFormItem> = {}): OrderFormItem {
46
- return {
47
- uniqueId: "uid-1",
48
- id: "sku-1",
49
- productId: "p-1",
50
- name: "T-Shirt",
51
- skuName: "T-Shirt M",
52
- price: 9990,
53
- listPrice: 12990,
54
- sellingPrice: 9990,
55
- quantity: 2,
56
- seller: "1",
57
- imageUrl: "http://example.com/img.jpg",
58
- detailUrl: "/p/t-shirt",
59
- additionalInfo: { brandName: "Brand", brandId: "b-1" },
60
- productCategoryIds: "/123/",
61
- productCategories: { "123": "Clothing" },
62
- availability: "available",
63
- measurementUnit: "un",
64
- unitMultiplier: 1,
65
- ...overrides,
66
- };
67
- }
68
-
69
- function tot(id: string, value: number, name = id): Totalizer {
70
- return { id, name, value };
71
- }
72
-
73
- describe("vtexOrderFormToMinicart", () => {
74
- it("returns an empty cart shape for an empty OrderForm", () => {
75
- const minicart = vtexOrderFormToMinicart(makeOrderForm());
76
- expect(minicart.storefront.items).toEqual([]);
77
- expect(minicart.storefront.subtotal).toBe(0);
78
- expect(minicart.storefront.discounts).toBe(0);
79
- expect(minicart.storefront.total).toBe(0);
80
- expect(minicart.storefront.shipping).toBeUndefined();
81
- });
82
-
83
- it("converts cents to major units across totalizers", () => {
84
- const orderForm = makeOrderForm({
85
- value: 19980,
86
- totalizers: [tot("Items", 25980), tot("Discounts", -6000), tot("Shipping", 1500)],
87
- items: [makeItem()],
88
- });
89
- const minicart = vtexOrderFormToMinicart(orderForm);
90
- expect(minicart.storefront.subtotal).toBeCloseTo(259.8, 2);
91
- expect(minicart.storefront.discounts).toBeCloseTo(60, 2);
92
- expect(minicart.storefront.shipping).toBeCloseTo(15, 2);
93
- expect(minicart.storefront.total).toBeCloseTo(199.8, 2);
94
- });
95
-
96
- it("Discounts are always non-negative even when VTEX returns a negative totalizer", () => {
97
- const orderForm = makeOrderForm({
98
- value: 9990,
99
- totalizers: [tot("Items", 9990), tot("Discounts", -1000)],
100
- items: [makeItem()],
101
- });
102
- const minicart = vtexOrderFormToMinicart(orderForm);
103
- expect(minicart.storefront.discounts).toBe(10);
104
- });
105
-
106
- it("omits shipping when no Shipping totalizer is present", () => {
107
- const orderForm = makeOrderForm({
108
- value: 9990,
109
- totalizers: [tot("Items", 9990)],
110
- items: [makeItem()],
111
- });
112
- const minicart = vtexOrderFormToMinicart(orderForm);
113
- expect(minicart.storefront.shipping).toBeUndefined();
114
- });
115
-
116
- it("maps OrderFormItem to MinicartItem with major-unit prices and analytics fields", () => {
117
- const orderForm = makeOrderForm({
118
- value: 19980,
119
- totalizers: [tot("Items", 19980)],
120
- items: [makeItem()],
121
- });
122
- const minicart = vtexOrderFormToMinicart(orderForm);
123
- const item = minicart.storefront.items[0];
124
- expect(item.item_id).toBe("sku-1");
125
- expect(item.item_group_id).toBe("p-1");
126
- expect(item.item_name).toBe("T-Shirt");
127
- expect(item.item_brand).toBe("Brand");
128
- expect(item.item_url).toBe("/p/t-shirt");
129
- expect(item.price).toBeCloseTo(99.9, 2);
130
- expect(item.listPrice).toBeCloseTo(129.9, 2);
131
- expect(item.discount).toBeCloseTo(30, 2);
132
- expect(item.quantity).toBe(2);
133
- expect(item.seller).toBe("1");
134
- expect(item.affiliation).toBe("1");
135
- });
136
-
137
- it("forces https on item images", () => {
138
- const orderForm = makeOrderForm({
139
- items: [makeItem({ imageUrl: "http://cdn.example.com/img.jpg" })],
140
- });
141
- const minicart = vtexOrderFormToMinicart(orderForm);
142
- expect(minicart.storefront.items[0].image).toBe("https://cdn.example.com/img.jpg");
143
- });
144
-
145
- it("propagates the marketing coupon onto every item and the storefront root", () => {
146
- const orderForm = makeOrderForm({
147
- items: [makeItem(), makeItem({ uniqueId: "uid-2", id: "sku-2" })],
148
- marketingData: { coupon: "SAVE10" },
149
- });
150
- const minicart = vtexOrderFormToMinicart(orderForm);
151
- expect(minicart.storefront.coupon).toBe("SAVE10");
152
- expect(minicart.storefront.items[0].coupon).toBe("SAVE10");
153
- expect(minicart.storefront.items[1].coupon).toBe("SAVE10");
154
- });
155
-
156
- it("uses opts overrides for free-shipping target, locale, checkout href and coupon toggle", () => {
157
- const minicart = vtexOrderFormToMinicart(makeOrderForm(), {
158
- freeShippingTarget: 250,
159
- locale: "en-US",
160
- checkoutHref: "/cart/go",
161
- enableCoupon: false,
162
- });
163
- expect(minicart.storefront.freeShippingTarget).toBe(250);
164
- expect(minicart.storefront.locale).toBe("en-US");
165
- expect(minicart.storefront.checkoutHref).toBe("/cart/go");
166
- expect(minicart.storefront.enableCoupon).toBe(false);
167
- });
168
-
169
- it("infers pt-BR locale from BRA countryCode when no override is given", () => {
170
- const orderForm = makeOrderForm({
171
- clientPreferencesData: { locale: "" },
172
- storePreferencesData: { ...baseStorePreferences, countryCode: "BRA" },
173
- });
174
- const minicart = vtexOrderFormToMinicart(orderForm);
175
- expect(minicart.storefront.locale).toBe("pt-BR");
176
- });
177
-
178
- it("preserves the raw OrderForm under .original for site escape hatches", () => {
179
- const orderForm = makeOrderForm({ orderFormId: "of-original" });
180
- const minicart = vtexOrderFormToMinicart(orderForm);
181
- expect(minicart.original).toBe(orderForm);
182
- expect(minicart.original.orderFormId).toBe("of-original");
183
- });
184
- });