@thorprovider/adapters 5.0.10 → 5.0.12

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 (37) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/index.d.mts +376 -16
  3. package/dist/index.d.ts +376 -16
  4. package/dist/index.js +1687 -528
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1680 -530
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/core/errors.ts +31 -0
  10. package/src/factory/index.ts +16 -6
  11. package/src/factory/provider-types.test.ts +32 -0
  12. package/src/factory/provider-types.ts +176 -0
  13. package/src/index.ts +6 -4
  14. package/src/providers/index.ts +1 -1
  15. package/src/providers/medusa/auth.test.ts +148 -25
  16. package/src/providers/medusa/auth.ts +129 -16
  17. package/src/providers/medusa/available-filters.test.ts +280 -0
  18. package/src/providers/medusa/catalog-facet-support.test.ts +715 -0
  19. package/src/providers/medusa/catalog-query.test.ts +422 -0
  20. package/src/providers/medusa/catalog-sort.test.ts +175 -0
  21. package/src/providers/medusa/customer-orders.test.ts +142 -0
  22. package/src/providers/medusa/index.test.ts +480 -0
  23. package/src/providers/medusa/index.ts +1592 -538
  24. package/src/providers/medusa/live-smoke.test.ts +68 -0
  25. package/src/providers/medusa/transforms.test.ts +222 -0
  26. package/src/providers/medusa/transforms.ts +152 -66
  27. package/src/providers/mock/catalog-query.test.ts +491 -0
  28. package/src/providers/mock/index.test.ts +66 -0
  29. package/src/providers/mock/index.ts +43 -1
  30. package/src/providers/mock/mock.ts +513 -3
  31. package/src/types/completion.ts +57 -0
  32. package/src/types/index.ts +7 -0
  33. package/.turbo/turbo-build.log +0 -18
  34. package/.turbo/turbo-test.log +0 -372
  35. package/.turbo/turbo-type-check.log +0 -0
  36. package/dist/.tsbuildinfo +0 -1
  37. package/publish.log +0 -93
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Adapter canonical query forwarding — E2 `storefront-catalog-search-filters`
3
+ * task 2.2.
4
+ *
5
+ * `getCatalogProducts` must accept a validated `CatalogQuery` and forward its
6
+ * scope, text, collections, sort, and pagination to the Medusa Store
7
+ * `/store/products` endpoint, reporting only the filters it actually applied.
8
+ * Any requested filter the backend cannot apply (price/stock/attributes) must
9
+ * return a typed `unsupported-query` outcome naming the fields, never a partial
10
+ * success, and must not issue a provider query. An empty route-scope ∩
11
+ * `categories` intersection is a valid zero-result query, never an unfiltered
12
+ * request.
13
+ *
14
+ * The HTTP boundary is intercepted with msw against a controlled fixture, so the
15
+ * emitted query parameters are asserted directly — the meaningful SDK seam.
16
+ */
17
+ import { describe, expect, it } from 'vitest'
18
+ import { http, HttpResponse } from 'msw'
19
+ import { server } from '../stelorder/client.test-helpers'
20
+ import { MedusaProvider } from './index'
21
+
22
+ const BASE = 'https://medusa.example.com'
23
+ const PUB_KEY = 'pk_test_abc123'
24
+ const CURRENCY = 'usd'
25
+
26
+ const CATEGORIES = [
27
+ {
28
+ id: 'pcat_x',
29
+ name: 'Category X',
30
+ handle: 'x',
31
+ description: null,
32
+ parent_category_id: null,
33
+ product_count: 3,
34
+ metadata: null,
35
+ },
36
+ {
37
+ id: 'pcat_y',
38
+ name: 'Category Y',
39
+ handle: 'y',
40
+ description: null,
41
+ parent_category_id: null,
42
+ product_count: 2,
43
+ metadata: null,
44
+ },
45
+ ]
46
+
47
+ const COLLECTIONS = [
48
+ { id: 'pcol_c1', handle: 'c1', title: 'Collection One', metadata: null },
49
+ { id: 'pcol_c2', handle: 'c2', title: 'Collection Two', metadata: null },
50
+ ]
51
+
52
+ const MEDUSA_PRODUCT = {
53
+ id: 'prod_01',
54
+ handle: 'test-product',
55
+ title: 'Test Product',
56
+ subtitle: null,
57
+ description: 'A test product',
58
+ status: 'published',
59
+ thumbnail: 'https://example.com/thumb.jpg',
60
+ images: [{ id: 'img_01', url: 'https://example.com/img.jpg', metadata: null }],
61
+ tags: [],
62
+ collection: null,
63
+ categories: [{ id: 'pcat_x', name: 'Category X', handle: 'x' }],
64
+ options: [],
65
+ variants: [
66
+ {
67
+ id: 'variant_01',
68
+ title: 'Default',
69
+ product_id: 'prod_01',
70
+ sku: 'SKU-001',
71
+ inventory_quantity: 10,
72
+ prices: [{ id: 'price_01', amount: 2500, currency_code: 'usd' }],
73
+ calculated_price: { calculated_amount: 2500, currency_code: 'usd' },
74
+ options: [],
75
+ },
76
+ ],
77
+ metadata: null,
78
+ created_at: '2026-01-01T00:00:00Z',
79
+ updated_at: '2026-01-01T00:00:00Z',
80
+ }
81
+
82
+ function makeProvider() {
83
+ return new MedusaProvider({
84
+ baseUrl: BASE,
85
+ publishableKey: PUB_KEY,
86
+ currencyCode: CURRENCY,
87
+ debug: false,
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Read a query parameter regardless of how the SDK serializes it: a scalar as
93
+ * `key`, or a list as `key[0]`/`key[]` (qs default array format).
94
+ */
95
+ function getParam(url: string, key: string): string | null {
96
+ const params = new URL(url).searchParams
97
+ if (params.has(key)) return params.get(key)
98
+ for (const [k, v] of params.entries()) {
99
+ if (k === `${key}[]` || k.startsWith(`${key}[`)) return v
100
+ }
101
+ return null
102
+ }
103
+
104
+ function getParamValues(url: string, key: string): string[] {
105
+ const out: string[] = []
106
+ for (const [k, v] of new URL(url).searchParams.entries()) {
107
+ if (k === key || k === `${key}[]` || k.startsWith(`${key}[`)) out.push(v)
108
+ }
109
+ return out
110
+ }
111
+
112
+ interface Fixture {
113
+ /** Every `/store/products` request URL (the provider query under test). */
114
+ productListUrls: string[]
115
+ /** Whether the backend supplies a total count on the product list. */
116
+ withCount: boolean
117
+ }
118
+
119
+ function installFixture(options: { withCount?: boolean } = {}): Fixture {
120
+ const fixture: Fixture = {
121
+ productListUrls: [],
122
+ withCount: options.withCount ?? true,
123
+ }
124
+ server.use(
125
+ // Thor capability probe is unrelated to the product listing; answer 404 so
126
+ // the fixture stays quiet and only product-list calls are captured.
127
+ http.get(`${BASE}/store/thor/capabilities`, () =>
128
+ HttpResponse.json({}, { status: 404 }),
129
+ ),
130
+ http.get(`${BASE}/store/product-categories`, ({ request }) => {
131
+ const handle = getParam(request.url, 'handle')
132
+ const match = handle
133
+ ? CATEGORIES.filter((c) => c.handle === handle)
134
+ : CATEGORIES
135
+ return HttpResponse.json({
136
+ product_categories: match,
137
+ count: match.length,
138
+ offset: 0,
139
+ limit: 1,
140
+ })
141
+ }),
142
+ http.get(`${BASE}/store/collections`, ({ request }) => {
143
+ const handle = getParam(request.url, 'handle')
144
+ const match = handle
145
+ ? COLLECTIONS.filter((c) => c.handle === handle)
146
+ : COLLECTIONS
147
+ return HttpResponse.json({
148
+ collections: match,
149
+ count: match.length,
150
+ offset: 0,
151
+ limit: 1,
152
+ })
153
+ }),
154
+ http.get(`${BASE}/store/products`, ({ request }) => {
155
+ fixture.productListUrls.push(request.url)
156
+ return HttpResponse.json({
157
+ products: [MEDUSA_PRODUCT],
158
+ ...(fixture.withCount ? { count: 42 } : {}),
159
+ offset: 0,
160
+ limit: 50,
161
+ })
162
+ }),
163
+ // The optional advanced endpoint is absent on this fixture: the facet
164
+ // support probe must hold price/stock/attribute filters.
165
+ http.get(`${BASE}/store/search`, () =>
166
+ HttpResponse.json(
167
+ { error: 'Failed to search products', message: 'search unavailable' },
168
+ { status: 500 },
169
+ ),
170
+ ),
171
+ http.get(`${BASE}/store/custom-attributes/category/:categoryId`, () =>
172
+ HttpResponse.json({ attributes: [], count: 0 }),
173
+ ),
174
+ )
175
+ return fixture
176
+ }
177
+
178
+ describe('MedusaProvider.getCatalogProducts — canonical query forwarding', () => {
179
+ it('forwards text as q and reports it as an applied filter', async () => {
180
+ const fixture = installFixture()
181
+ const outcome = await makeProvider().getCatalogProducts({
182
+ text: 'taladro',
183
+ sort: 'featured',
184
+ page: 1,
185
+ pageSize: 50,
186
+ })
187
+
188
+ expect(outcome.ok).toBe(true)
189
+ if (!outcome.ok) return
190
+ expect(fixture.productListUrls).toHaveLength(1)
191
+ expect(getParam(fixture.productListUrls[0], 'q')).toBe('taladro')
192
+ expect(outcome.results.appliedFilters.text).toBe('taladro')
193
+ expect(outcome.results.products).toHaveLength(1)
194
+ expect(outcome.results.total).toBe(42)
195
+ })
196
+
197
+ it('retains the route category scope and resolves its handle to a backend ID', async () => {
198
+ const fixture = installFixture()
199
+ const outcome = await makeProvider().getCatalogProducts({
200
+ categoryScope: 'x',
201
+ page: 1,
202
+ pageSize: 50,
203
+ })
204
+
205
+ expect(outcome.ok).toBe(true)
206
+ if (!outcome.ok) return
207
+ expect(getParamValues(fixture.productListUrls[0], 'category_id')).toEqual([
208
+ 'pcat_x',
209
+ ])
210
+ expect(outcome.results.appliedFilters.categories).toEqual(['x'])
211
+ })
212
+
213
+ it('intersects the categories filter with the route scope', async () => {
214
+ const fixture = installFixture()
215
+ const outcome = await makeProvider().getCatalogProducts({
216
+ categoryScope: 'x',
217
+ categories: ['x', 'y'],
218
+ page: 1,
219
+ pageSize: 50,
220
+ })
221
+
222
+ expect(outcome.ok).toBe(true)
223
+ if (!outcome.ok) return
224
+ expect(getParamValues(fixture.productListUrls[0], 'category_id')).toEqual([
225
+ 'pcat_x',
226
+ ])
227
+ expect(outcome.results.appliedFilters.categories).toEqual(['x'])
228
+ })
229
+
230
+ it('treats an empty route-scope ∩ categories intersection as zero results with no unfiltered request', async () => {
231
+ const fixture = installFixture()
232
+ const outcome = await makeProvider().getCatalogProducts({
233
+ categoryScope: 'x',
234
+ categories: ['y'],
235
+ page: 1,
236
+ pageSize: 50,
237
+ })
238
+
239
+ expect(outcome.ok).toBe(true)
240
+ if (!outcome.ok) return
241
+ expect(outcome.results.products).toEqual([])
242
+ expect(outcome.results.appliedFilters.categories).toEqual([])
243
+ expect(fixture.productListUrls).toHaveLength(0)
244
+ })
245
+
246
+ it('resolves collection handles to backend IDs and reports them applied', async () => {
247
+ const fixture = installFixture()
248
+ const outcome = await makeProvider().getCatalogProducts({
249
+ collections: ['c1'],
250
+ page: 1,
251
+ pageSize: 50,
252
+ })
253
+
254
+ expect(outcome.ok).toBe(true)
255
+ if (!outcome.ok) return
256
+ expect(getParamValues(fixture.productListUrls[0], 'collection_id')).toEqual([
257
+ 'pcol_c1',
258
+ ])
259
+ expect(outcome.results.appliedFilters.collections).toEqual(['c1'])
260
+ })
261
+
262
+ it('translates canonical sort keys to the backend order and omits featured', async () => {
263
+ const cases: Array<[string, string | null]> = [
264
+ ['name', 'title'],
265
+ ['newest', '-created_at'],
266
+ ['featured', null],
267
+ ]
268
+
269
+ for (const [sort, order] of cases) {
270
+ const fixture = installFixture()
271
+ const outcome = await makeProvider().getCatalogProducts({
272
+ sort: sort as 'name' | 'newest' | 'featured',
273
+ page: 1,
274
+ pageSize: 50,
275
+ })
276
+ expect(outcome.ok).toBe(true)
277
+ expect(getParam(fixture.productListUrls[0], 'order')).toBe(order)
278
+ }
279
+ })
280
+
281
+ it('maps page/pageSize to limit and offset = (page - 1) * pageSize', async () => {
282
+ const fixture = installFixture()
283
+ await makeProvider().getCatalogProducts({ page: 3, pageSize: 20 })
284
+
285
+ const params = new URL(fixture.productListUrls[0]).searchParams
286
+ expect(params.get('limit')).toBe('20')
287
+ expect(params.get('offset')).toBe('40')
288
+ })
289
+
290
+ it('defaults pagination to the canonical page size and first page', async () => {
291
+ const fixture = installFixture()
292
+ await makeProvider().getCatalogProducts({})
293
+
294
+ const params = new URL(fixture.productListUrls[0]).searchParams
295
+ expect(params.get('limit')).toBe('50')
296
+ expect(params.get('offset')).toBe('0')
297
+ })
298
+
299
+ it('reflects every applied requested filter (text + categories + collections)', async () => {
300
+ const fixture = installFixture()
301
+ const outcome = await makeProvider().getCatalogProducts({
302
+ text: 'term',
303
+ categoryScope: 'x',
304
+ categories: ['x'],
305
+ collections: ['c1'],
306
+ sort: 'name',
307
+ page: 2,
308
+ pageSize: 10,
309
+ })
310
+
311
+ expect(outcome.ok).toBe(true)
312
+ if (!outcome.ok) return
313
+ expect(outcome.results.appliedFilters).toEqual({
314
+ text: 'term',
315
+ categories: ['x'],
316
+ collections: ['c1'],
317
+ })
318
+ const params = new URL(fixture.productListUrls[0]).searchParams
319
+ expect(params.get('q')).toBe('term')
320
+ expect(getParamValues(fixture.productListUrls[0], 'category_id')).toEqual([
321
+ 'pcat_x',
322
+ ])
323
+ expect(getParamValues(fixture.productListUrls[0], 'collection_id')).toEqual([
324
+ 'pcol_c1',
325
+ ])
326
+ expect(params.get('limit')).toBe('10')
327
+ expect(params.get('offset')).toBe('10')
328
+ })
329
+
330
+ it('requests the product thumbnail so a listing can resolve a featured image', async () => {
331
+ const fixture = installFixture()
332
+ await makeProvider().getCatalogProducts({ page: 1, pageSize: 50 })
333
+
334
+ const fields = getParam(fixture.productListUrls[0], 'fields')
335
+ expect(fields).not.toBeNull()
336
+ // `+thumbnail` ADDS to Medusa's defaults (a bare `thumbnail` would replace
337
+ // every default field, dropping variants/prices).
338
+ expect(fields).toContain('+thumbnail')
339
+ })
340
+
341
+ it('maps a returned thumbnail to featuredImage on a basic catalog product', async () => {
342
+ const fixture = installFixture()
343
+ const outcome = await makeProvider().getCatalogProducts({
344
+ page: 1,
345
+ pageSize: 50,
346
+ })
347
+
348
+ expect(outcome.ok).toBe(true)
349
+ if (!outcome.ok) return
350
+ expect(fixture.productListUrls).toHaveLength(1)
351
+ const product = outcome.results.products[0]
352
+ expect(product.featuredImage?.url).toBe('https://example.com/thumb.jpg')
353
+ expect(product.images?.map((img) => img.url)).toEqual([
354
+ 'https://example.com/img.jpg',
355
+ ])
356
+ })
357
+
358
+ it('omits total when the backend supplies none (never manufactured)', async () => {
359
+ const fixture = installFixture({ withCount: false })
360
+ const outcome = await makeProvider().getCatalogProducts({ page: 1, pageSize: 50 })
361
+
362
+ expect(outcome.ok).toBe(true)
363
+ if (!outcome.ok) return
364
+ expect(fixture.productListUrls).toHaveLength(1)
365
+ expect(outcome.results.total).toBeUndefined()
366
+ })
367
+
368
+ it('rejects an unadvertised sort instead of silently defaulting', async () => {
369
+ const fixture = installFixture()
370
+ const outcome = await makeProvider().getCatalogProducts({
371
+ sort: 'price-asc',
372
+ } as never)
373
+
374
+ expect(outcome.ok).toBe(false)
375
+ if (outcome.ok) return
376
+ expect(outcome.error.code).toBe('unsupported-query')
377
+ expect(outcome.error.fields).toContain('sort')
378
+ expect(fixture.productListUrls).toHaveLength(0)
379
+ })
380
+
381
+ it.each([
382
+ ['priceMin', { priceMin: 10 }],
383
+ ['priceMax', { priceMax: 99 }],
384
+ ['inStock', { inStock: true }],
385
+ ['attr.brand', { attributes: { brand: ['acme'] } }],
386
+ ])(
387
+ 'rejects an unsupported requested filter (%s) with a typed unsupported-query and no provider query',
388
+ async (field, partial) => {
389
+ const fixture = installFixture()
390
+ const outcome = await makeProvider().getCatalogProducts({
391
+ page: 1,
392
+ pageSize: 50,
393
+ ...(partial as Record<string, unknown>),
394
+ } as never)
395
+
396
+ expect(outcome.ok).toBe(false)
397
+ if (outcome.ok) return
398
+ expect(outcome.error.code).toBe('unsupported-query')
399
+ expect(outcome.error.fields).toContain(field)
400
+ expect(outcome.error.repair.remove).toContain(field)
401
+ // No partial success and no provider query call at all.
402
+ expect(fixture.productListUrls).toHaveLength(0)
403
+ },
404
+ )
405
+
406
+ it('rejects when several unsupported filters are requested, naming every field', async () => {
407
+ const fixture = installFixture()
408
+ const outcome = await makeProvider().getCatalogProducts({
409
+ priceMin: 10,
410
+ inStock: true,
411
+ page: 1,
412
+ pageSize: 50,
413
+ })
414
+
415
+ expect(outcome.ok).toBe(false)
416
+ if (outcome.ok) return
417
+ expect(outcome.error.fields).toEqual(
418
+ expect.arrayContaining(['priceMin', 'inStock']),
419
+ )
420
+ expect(fixture.productListUrls).toHaveLength(0)
421
+ })
422
+ })
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Adapter sort truthfulness — E2 `storefront-catalog-search-filters` task 2.1.
3
+ *
4
+ * `getCategoryProducts` must translate canonical Thor sort keys through the
5
+ * same mapping `getProducts` uses (`name → title`, `newest → -created_at`,
6
+ * `featured → default`), never pass the raw canonical key to Medusa's `order`
7
+ * field (the Store API rejects a disallowed field with a 400). The adapter must
8
+ * advertise exactly the sorts it can apply, with no global price/rating sort.
9
+ *
10
+ * The HTTP boundary is intercepted with msw, so the emitted `order` query
11
+ * parameter is asserted directly — the meaningful SDK seam.
12
+ */
13
+ import { describe, expect, it } from 'vitest'
14
+ import { http, HttpResponse } from 'msw'
15
+ import { server } from '../stelorder/client.test-helpers'
16
+ import { MedusaProvider } from './index'
17
+
18
+ const BASE = 'https://medusa.example.com'
19
+ const PUB_KEY = 'pk_test_abc123'
20
+ const CURRENCY = 'usd'
21
+
22
+ const MEDUSA_CATEGORY = {
23
+ id: 'pcat_01',
24
+ name: 'Electronics',
25
+ handle: 'electronics',
26
+ description: null,
27
+ parent_category_id: null,
28
+ product_count: 1,
29
+ metadata: null,
30
+ }
31
+
32
+ const MEDUSA_PRODUCT = {
33
+ id: 'prod_01',
34
+ handle: 'test-product',
35
+ title: 'Test Product',
36
+ subtitle: null,
37
+ description: 'A test product',
38
+ status: 'published',
39
+ thumbnail: 'https://example.com/thumb.jpg',
40
+ images: [{ id: 'img_01', url: 'https://example.com/img.jpg', metadata: null }],
41
+ tags: [],
42
+ collection: null,
43
+ categories: [{ id: 'pcat_01', name: 'Electronics', handle: 'electronics' }],
44
+ options: [],
45
+ variants: [
46
+ {
47
+ id: 'variant_01',
48
+ title: 'Default',
49
+ product_id: 'prod_01',
50
+ sku: 'SKU-001',
51
+ inventory_quantity: 10,
52
+ prices: [{ id: 'price_01', amount: 2500, currency_code: 'usd' }],
53
+ calculated_price: { calculated_amount: 2500, currency_code: 'usd' },
54
+ options: [],
55
+ },
56
+ ],
57
+ metadata: null,
58
+ created_at: '2026-01-01T00:00:00Z',
59
+ updated_at: '2026-01-01T00:00:00Z',
60
+ }
61
+
62
+ function makeProvider() {
63
+ return new MedusaProvider({
64
+ baseUrl: BASE,
65
+ publishableKey: PUB_KEY,
66
+ currencyCode: CURRENCY,
67
+ debug: false,
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Intercept category lookup + product listing and capture the emitted `order`
73
+ * query parameter of the product-list request.
74
+ */
75
+ function captureProductListOrder(): { orders: (string | null)[] } {
76
+ const orders: (string | null)[] = []
77
+ server.use(
78
+ http.get(`${BASE}/store/product-categories`, () =>
79
+ HttpResponse.json({
80
+ product_categories: [MEDUSA_CATEGORY],
81
+ count: 1,
82
+ offset: 0,
83
+ limit: 1,
84
+ }),
85
+ ),
86
+ http.get(`${BASE}/store/products`, ({ request }) => {
87
+ const url = new URL(request.url)
88
+ orders.push(url.searchParams.get('order'))
89
+ return HttpResponse.json({
90
+ products: [MEDUSA_PRODUCT],
91
+ count: 1,
92
+ offset: 0,
93
+ limit: 50,
94
+ })
95
+ }),
96
+ )
97
+ return { orders }
98
+ }
99
+
100
+ describe('MedusaProvider — catalog sort translation and advertisement', () => {
101
+ it('advertises exactly the canonical sorts it can apply, with no price/rating sort', () => {
102
+ const provider = makeProvider()
103
+
104
+ expect(provider.supportedCatalogSortKeys).toEqual(['featured', 'name', 'newest'])
105
+ expect(provider.supportedCatalogSortKeys).not.toContain('price-asc')
106
+ expect(provider.supportedCatalogSortKeys).not.toContain('price-desc')
107
+ expect(provider.supportedCatalogSortKeys).not.toContain('rating')
108
+ })
109
+
110
+ it('getCategoryProducts maps name → title', async () => {
111
+ const { orders } = captureProductListOrder()
112
+ await makeProvider().getCategoryProducts('electronics', { sortKey: 'name' })
113
+ expect(orders).toEqual(['title'])
114
+ })
115
+
116
+ it('getCategoryProducts maps newest → -created_at', async () => {
117
+ const { orders } = captureProductListOrder()
118
+ await makeProvider().getCategoryProducts('electronics', { sortKey: 'newest' })
119
+ expect(orders).toEqual(['-created_at'])
120
+ })
121
+
122
+ it('getCategoryProducts omits order for featured (server default)', async () => {
123
+ const { orders } = captureProductListOrder()
124
+ await makeProvider().getCategoryProducts('electronics', { sortKey: 'featured' })
125
+ expect(orders).toEqual([null])
126
+ })
127
+
128
+ it('never emits a raw canonical key as the Medusa order field', async () => {
129
+ for (const sortKey of ['featured', 'name', 'newest'] as const) {
130
+ const { orders } = captureProductListOrder()
131
+ await makeProvider().getCategoryProducts('electronics', { sortKey })
132
+ expect(orders[0]).not.toBe('featured')
133
+ expect(orders[0]).not.toBe('name')
134
+ expect(orders[0]).not.toBe('newest')
135
+ }
136
+ })
137
+
138
+ it('omits a page-local price/rating sort instead of sending it as a global order (no default price sort, not mislabelled)', async () => {
139
+ // The legacy `SortOption` price/rating keys are page-local orderings the
140
+ // backend cannot apply globally. Pre-change `getCategoryProducts` forwarded
141
+ // the raw key to `order` (`order=price-asc`), presenting a page-local price
142
+ // sort as a global backend sort. They must be omitted (server default).
143
+ for (const sortKey of ['price-asc', 'price-desc', 'rating'] as const) {
144
+ const { orders } = captureProductListOrder()
145
+ await makeProvider().getCategoryProducts('electronics', { sortKey })
146
+ expect(orders).toEqual([null])
147
+ expect(orders[0]).not.toBe(sortKey)
148
+ }
149
+ })
150
+
151
+ it('getProducts maps the same canonical keys through the same mapping', async () => {
152
+ const expected: Record<string, string | null> = {
153
+ featured: null,
154
+ name: 'title',
155
+ newest: '-created_at',
156
+ }
157
+
158
+ for (const [sortKey, order] of Object.entries(expected)) {
159
+ const orders: (string | null)[] = []
160
+ server.use(
161
+ http.get(`${BASE}/store/products`, ({ request }) => {
162
+ orders.push(new URL(request.url).searchParams.get('order'))
163
+ return HttpResponse.json({
164
+ products: [MEDUSA_PRODUCT],
165
+ count: 1,
166
+ offset: 0,
167
+ limit: 50,
168
+ })
169
+ }),
170
+ )
171
+ await makeProvider().getProducts({ sortKey })
172
+ expect(orders).toEqual([order])
173
+ }
174
+ })
175
+ })