@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.
Files changed (109) hide show
  1. package/package.json +67 -0
  2. package/src/README.md +6 -0
  3. package/src/__tests__/client-segment-cookie.test.ts +255 -0
  4. package/src/__tests__/client-set-cookie-forward.test.ts +257 -0
  5. package/src/actions/address.ts +250 -0
  6. package/src/actions/analytics/sendEvent.ts +86 -0
  7. package/src/actions/auth.ts +333 -0
  8. package/src/actions/checkout.ts +522 -0
  9. package/src/actions/index.ts +11 -0
  10. package/src/actions/masterData.ts +168 -0
  11. package/src/actions/misc.ts +188 -0
  12. package/src/actions/newsletter.ts +105 -0
  13. package/src/actions/orders.ts +34 -0
  14. package/src/actions/profile.ts +201 -0
  15. package/src/actions/session.ts +85 -0
  16. package/src/actions/trigger.ts +43 -0
  17. package/src/actions/wishlist.ts +114 -0
  18. package/src/client.ts +667 -0
  19. package/src/commerceLoaders.ts +261 -0
  20. package/src/hooks/__tests__/createUseCart.test.ts +184 -0
  21. package/src/hooks/__tests__/createUseUser.test.ts +48 -0
  22. package/src/hooks/__tests__/createUseWishlist.test.ts +87 -0
  23. package/src/hooks/createUseCart.ts +360 -0
  24. package/src/hooks/createUseUser.ts +153 -0
  25. package/src/hooks/createUseWishlist.ts +242 -0
  26. package/src/hooks/index.ts +19 -0
  27. package/src/hooks/useAutocomplete.ts +83 -0
  28. package/src/hooks/useCart.ts +243 -0
  29. package/src/hooks/useUser.ts +78 -0
  30. package/src/hooks/useWishlist.ts +119 -0
  31. package/src/index.ts +17 -0
  32. package/src/invoke.ts +181 -0
  33. package/src/loaders/ProductDetailsPage.ts +1 -0
  34. package/src/loaders/ProductList.ts +1 -0
  35. package/src/loaders/ProductListingPage.ts +1 -0
  36. package/src/loaders/address.ts +115 -0
  37. package/src/loaders/autocomplete.ts +58 -0
  38. package/src/loaders/brands.ts +44 -0
  39. package/src/loaders/cart.ts +52 -0
  40. package/src/loaders/catalog.ts +163 -0
  41. package/src/loaders/collections.ts +55 -0
  42. package/src/loaders/index.ts +19 -0
  43. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +20 -0
  44. package/src/loaders/intelligentSearch/productDetailsPage.ts +95 -0
  45. package/src/loaders/intelligentSearch/productList.ts +154 -0
  46. package/src/loaders/intelligentSearch/productListingPage.ts +506 -0
  47. package/src/loaders/intelligentSearch/suggestions.ts +46 -0
  48. package/src/loaders/legacy/productDetailsPage.ts +1 -0
  49. package/src/loaders/legacy/productList.ts +1 -0
  50. package/src/loaders/legacy/relatedProductsLoader.ts +81 -0
  51. package/src/loaders/legacy.ts +635 -0
  52. package/src/loaders/logistics.ts +111 -0
  53. package/src/loaders/minicart.ts +78 -0
  54. package/src/loaders/navbar.ts +26 -0
  55. package/src/loaders/orders.ts +92 -0
  56. package/src/loaders/pageType.ts +68 -0
  57. package/src/loaders/payment.ts +97 -0
  58. package/src/loaders/productListFull.ts +159 -0
  59. package/src/loaders/profile.ts +138 -0
  60. package/src/loaders/promotion.ts +30 -0
  61. package/src/loaders/search.ts +124 -0
  62. package/src/loaders/session.ts +83 -0
  63. package/src/loaders/user.ts +87 -0
  64. package/src/loaders/wishlist.ts +89 -0
  65. package/src/loaders/wishlistProducts.ts +69 -0
  66. package/src/loaders/workflow/products.ts +64 -0
  67. package/src/loaders/workflow.ts +316 -0
  68. package/src/logo.png +0 -0
  69. package/src/middleware.ts +240 -0
  70. package/src/mod.ts +152 -0
  71. package/src/registry.ts +9 -0
  72. package/src/types.ts +248 -0
  73. package/src/utils/__tests__/cookieSanitizer.test.ts +162 -0
  74. package/src/utils/__tests__/fetch.test.ts +80 -0
  75. package/src/utils/__tests__/fetchCache.test.ts +112 -0
  76. package/src/utils/__tests__/instrumentedFetch.test.ts +158 -0
  77. package/src/utils/__tests__/intelligentSearch.test.ts +88 -0
  78. package/src/utils/__tests__/minicart.test.ts +184 -0
  79. package/src/utils/__tests__/operationRouter.test.ts +227 -0
  80. package/src/utils/__tests__/resourceRange.test.ts +32 -0
  81. package/src/utils/__tests__/sitemap.test.ts +185 -0
  82. package/src/utils/__tests__/slugify.test.ts +31 -0
  83. package/src/utils/__tests__/transform.test.ts +698 -0
  84. package/src/utils/accountLoaders.ts +203 -0
  85. package/src/utils/authHelpers.ts +85 -0
  86. package/src/utils/batch.ts +18 -0
  87. package/src/utils/cookieSanitizer.ts +173 -0
  88. package/src/utils/cookies.ts +167 -0
  89. package/src/utils/enrichment.ts +560 -0
  90. package/src/utils/fetch.ts +107 -0
  91. package/src/utils/fetchCache.ts +222 -0
  92. package/src/utils/index.ts +19 -0
  93. package/src/utils/instrumentedFetch.ts +78 -0
  94. package/src/utils/intelligentSearch.ts +96 -0
  95. package/src/utils/legacy.ts +139 -0
  96. package/src/utils/minicart.ts +139 -0
  97. package/src/utils/operationRouter.ts +139 -0
  98. package/src/utils/pickAndOmit.ts +25 -0
  99. package/src/utils/proxy.ts +435 -0
  100. package/src/utils/resourceRange.ts +10 -0
  101. package/src/utils/segment.ts +152 -0
  102. package/src/utils/similars.ts +37 -0
  103. package/src/utils/sitemap.ts +282 -0
  104. package/src/utils/slugCache.ts +28 -0
  105. package/src/utils/slugify.ts +13 -0
  106. package/src/utils/transform.ts +1509 -0
  107. package/src/utils/types.ts +1884 -0
  108. package/src/utils/vtexId.ts +138 -0
  109. package/tsconfig.json +7 -0
@@ -0,0 +1,158 @@
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
+ });
@@ -0,0 +1,88 @@
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
+ });
@@ -0,0 +1,184 @@
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
+ });
@@ -0,0 +1,227 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { vtexOperationRouter } from "../operationRouter";
3
+
4
+ describe("vtexOperationRouter", () => {
5
+ const acct = "https://store.vtexcommercestable.com.br";
6
+
7
+ describe("Intelligent Search", () => {
8
+ it("captures the IS endpoint as the operation suffix", () => {
9
+ expect(
10
+ vtexOperationRouter(
11
+ `${acct}/api/io/_v/api/intelligent-search/product_search/electronics`,
12
+ "GET",
13
+ ),
14
+ ).toBe("intelligent-search.product_search");
15
+ expect(
16
+ vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/top_searches`, "GET"),
17
+ ).toBe("intelligent-search.top_searches");
18
+ expect(vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/facets`, "GET")).toBe(
19
+ "intelligent-search.facets",
20
+ );
21
+ expect(
22
+ vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/search_suggestions`, "GET"),
23
+ ).toBe("intelligent-search.search_suggestions");
24
+ });
25
+
26
+ it("strips query string before matching", () => {
27
+ expect(
28
+ vtexOperationRouter(
29
+ `${acct}/api/io/_v/api/intelligent-search/product_search?query=foo&sort=price`,
30
+ "GET",
31
+ ),
32
+ ).toBe("intelligent-search.product_search");
33
+ });
34
+ });
35
+
36
+ describe("Checkout / orderForm", () => {
37
+ it("differentiates create vs get on orderForm", () => {
38
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm`, "POST")).toBe(
39
+ "checkout.orderform.create",
40
+ );
41
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm`, "GET")).toBe(
42
+ "checkout.orderform.get",
43
+ );
44
+ });
45
+
46
+ it("differentiates items add/update/remove by HTTP method", () => {
47
+ const url = `${acct}/api/checkout/pub/orderForm/abc123/items`;
48
+ expect(vtexOperationRouter(url, "POST")).toBe("checkout.orderform.items.add");
49
+ expect(vtexOperationRouter(url, "PATCH")).toBe("checkout.orderform.items.update");
50
+ expect(vtexOperationRouter(url, "PUT")).toBe("checkout.orderform.items.update");
51
+ expect(vtexOperationRouter(url, "DELETE")).toBe("checkout.orderform.items.remove");
52
+ });
53
+
54
+ it("recognizes the /items/update legacy mass-update endpoint", () => {
55
+ expect(
56
+ vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc123/items/update`, "POST"),
57
+ ).toBe("checkout.orderform.items.update");
58
+ });
59
+
60
+ it("handles coupons, profile, shippingData, paymentData", () => {
61
+ const id = "abc123";
62
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/coupons`, "POST")).toBe(
63
+ "checkout.orderform.coupons",
64
+ );
65
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/profile`, "POST")).toBe(
66
+ "checkout.orderform.profile",
67
+ );
68
+ expect(
69
+ vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/shippingData/...`, "POST"),
70
+ ).toBe("checkout.orderform.shipping");
71
+ expect(
72
+ vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/paymentData/...`, "POST"),
73
+ ).toBe("checkout.orderform.payment");
74
+ });
75
+
76
+ it("matches the singleton orderForm/{id} root with the right method", () => {
77
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc`, "GET")).toBe(
78
+ "checkout.orderform.get",
79
+ );
80
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc`, "PATCH")).toBe(
81
+ "checkout.orderform.update",
82
+ );
83
+ });
84
+
85
+ it("maps simulation, regions, postal-code", () => {
86
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForms/simulation`, "POST")).toBe(
87
+ "checkout.simulation",
88
+ );
89
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/regions`, "GET")).toBe(
90
+ "checkout.regions",
91
+ );
92
+ expect(vtexOperationRouter(`${acct}/api/checkout/pub/postal-code/BRA/01310`, "GET")).toBe(
93
+ "checkout.postal-code",
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("Sessions + segments", () => {
99
+ it("differentiates sessions GET vs POST", () => {
100
+ expect(vtexOperationRouter(`${acct}/api/sessions`, "GET")).toBe("sessions.get");
101
+ expect(vtexOperationRouter(`${acct}/api/sessions`, "POST")).toBe("sessions.update");
102
+ });
103
+
104
+ it("matches segments", () => {
105
+ expect(vtexOperationRouter(`${acct}/api/segments/abc-123`, "GET")).toBe("segments.get");
106
+ });
107
+ });
108
+
109
+ describe("Catalog System", () => {
110
+ it("captures the most-specific catalog endpoints first", () => {
111
+ expect(
112
+ vtexOperationRouter(`${acct}/api/catalog_system/pub/portal/pagetype/eletronicos`, "GET"),
113
+ ).toBe("catalog.pagetype");
114
+ expect(
115
+ vtexOperationRouter(
116
+ `${acct}/api/catalog_system/pub/products/crossselling/whoboughtalsobought/123`,
117
+ "GET",
118
+ ),
119
+ ).toBe("catalog.crossselling.whoboughtalsobought");
120
+ expect(
121
+ vtexOperationRouter(`${acct}/api/catalog_system/pub/products/variations/123`, "GET"),
122
+ ).toBe("catalog.products.variations");
123
+ expect(
124
+ vtexOperationRouter(`${acct}/api/catalog_system/pub/products/search/?fq=x`, "GET"),
125
+ ).toBe("catalog.products.search");
126
+ expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/facets/search/x`, "GET")).toBe(
127
+ "catalog.facets.search",
128
+ );
129
+ expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/category/tree/3`, "GET")).toBe(
130
+ "catalog.category.tree",
131
+ );
132
+ expect(
133
+ vtexOperationRouter(`${acct}/api/catalog_system/pvt/sku/stockkeepingunitbyid/123`, "GET"),
134
+ ).toBe("catalog.sku");
135
+ });
136
+
137
+ it("falls back to catalog.other for unrecognized catalog paths", () => {
138
+ expect(
139
+ vtexOperationRouter(`${acct}/api/catalog_system/pvt/specification/groupGet/123`, "GET"),
140
+ ).toBe("catalog.specification");
141
+ expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/brand/list`, "GET")).toBe(
142
+ "catalog.brand",
143
+ );
144
+ });
145
+ });
146
+
147
+ describe("Masterdata", () => {
148
+ it("encodes the entity name as the operation suffix", () => {
149
+ expect(vtexOperationRouter(`${acct}/api/dataentities/AD/search`, "GET")).toBe(
150
+ "masterdata.AD",
151
+ );
152
+ expect(vtexOperationRouter(`${acct}/api/dataentities/wishlist_lists/documents`, "POST")).toBe(
153
+ "masterdata.wishlist_lists",
154
+ );
155
+ });
156
+ });
157
+
158
+ describe("OMS", () => {
159
+ it("differentiates orders list vs cancel", () => {
160
+ expect(vtexOperationRouter(`${acct}/api/oms/user/orders`, "GET")).toBe("oms.orders");
161
+ expect(vtexOperationRouter(`${acct}/api/oms/user/orders/v999-01/cancel`, "POST")).toBe(
162
+ "oms.orders.cancel",
163
+ );
164
+ expect(vtexOperationRouter(`${acct}/api/oms/pvt/orders/v999-01`, "GET")).toBe(
165
+ "oms.orders.pvt",
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("VTEX ID", () => {
171
+ it("maps the auth surface", () => {
172
+ expect(vtexOperationRouter(`${acct}/api/vtexid/pub/logout?scope=x`, "GET")).toBe(
173
+ "vtexid.logout",
174
+ );
175
+ expect(vtexOperationRouter(`${acct}/api/vtexid/pub/authentication/start`, "GET")).toBe(
176
+ "vtexid.authentication.start",
177
+ );
178
+ expect(
179
+ vtexOperationRouter(`${acct}/api/vtexid/pub/authentication/classic/validate`, "POST"),
180
+ ).toBe("vtexid.authentication.validate");
181
+ expect(vtexOperationRouter(`${acct}/api/vtexid/pub/authenticated/user`, "GET")).toBe(
182
+ "vtexid.user",
183
+ );
184
+ });
185
+
186
+ it("falls back to vtexid.other for unmapped vtexid paths", () => {
187
+ expect(vtexOperationRouter(`${acct}/api/vtexid/pub/refreshtoken`, "POST")).toBe(
188
+ "vtexid.other",
189
+ );
190
+ });
191
+ });
192
+
193
+ describe("VTEX IO + GraphQL", () => {
194
+ it("matches the IO private graphql endpoint", () => {
195
+ expect(vtexOperationRouter(`https://store.myvtex.com/_v/private/graphql/v1`, "POST")).toBe(
196
+ "io.graphql",
197
+ );
198
+ });
199
+
200
+ it("matches the IO segment endpoint", () => {
201
+ expect(
202
+ vtexOperationRouter(`https://store.myvtex.com/_v/segment/admin-pvt/whatever`, "GET"),
203
+ ).toBe("io.segment");
204
+ });
205
+ });
206
+
207
+ describe("Edge cases", () => {
208
+ it("returns undefined for fully unrecognized URLs", () => {
209
+ expect(vtexOperationRouter(`${acct}/somethingelse/random`, "GET")).toBeUndefined();
210
+ });
211
+
212
+ it("does not throw on unparseable URLs and still tries to match", () => {
213
+ expect(vtexOperationRouter("not-a-real-url", "GET")).toBeUndefined();
214
+ expect(vtexOperationRouter("/api/sessions?x=1", "GET")).toBe("sessions.get");
215
+ });
216
+
217
+ it("is case-insensitive on the method", () => {
218
+ expect(vtexOperationRouter(`${acct}/api/sessions`, "post")).toBe("sessions.update");
219
+ expect(vtexOperationRouter(`${acct}/api/sessions`, "Get")).toBe("sessions.get");
220
+ });
221
+
222
+ it("recognizes sitemap.xml + sitemap-products-0.xml", () => {
223
+ expect(vtexOperationRouter(`${acct}/sitemap.xml`, "GET")).toBe("sitemap");
224
+ expect(vtexOperationRouter(`${acct}/sitemap-products-0.xml`, "GET")).toBe("sitemap");
225
+ });
226
+ });
227
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resourceRange } from "../resourceRange";
3
+
4
+ describe("resourceRange", () => {
5
+ it("returns from=0 and to=take when skip is 0", () => {
6
+ expect(resourceRange(0, 50)).toEqual({ from: 0, to: 50 });
7
+ });
8
+
9
+ it("caps take at 100", () => {
10
+ expect(resourceRange(0, 200)).toEqual({ from: 0, to: 100 });
11
+ });
12
+
13
+ it("applies skip offset", () => {
14
+ expect(resourceRange(10, 50)).toEqual({ from: 10, to: 60 });
15
+ });
16
+
17
+ it("handles skip + take > 100 by capping take", () => {
18
+ expect(resourceRange(50, 200)).toEqual({ from: 50, to: 150 });
19
+ });
20
+
21
+ it("treats negative skip as 0", () => {
22
+ expect(resourceRange(-5, 10)).toEqual({ from: 0, to: 10 });
23
+ });
24
+
25
+ it("handles zero take", () => {
26
+ expect(resourceRange(0, 0)).toEqual({ from: 0, to: 0 });
27
+ });
28
+
29
+ it("handles take of exactly 100", () => {
30
+ expect(resourceRange(0, 100)).toEqual({ from: 0, to: 100 });
31
+ });
32
+ });