@faststore/core 4.7.0-dev.7 → 4.7.0-dev.9

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.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.6 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.0-dev.8 generate /home/runner/work/faststore/faststore/packages/core
3
3
  > pnpm run gen-types && pnpm run cache-graphql
4
4
 
5
5
 
6
- > @faststore/core@4.7.0-dev.6 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.7.0-dev.8 gen-types /home/runner/work/faststore/faststore/packages/core
7
7
  > node ../cli/bin/run generate-types .
8
8
 
9
9
  [STARTED] Parse Configuration
@@ -19,7 +19,7 @@
19
19
  [COMPLETED] Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
20
20
  [COMPLETED] Generate outputs
21
21
 
22
- > @faststore/core@4.7.0-dev.6 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.7.0-dev.8 cache-graphql /home/runner/work/faststore/faststore/packages/core
23
23
  > node ../cli/bin/run cache-graphql --config=./discovery.config.default.js --queries=./@generated/persisted-documents.json
24
24
 
25
25
  [Info] - Config file location: /home/runner/work/faststore/faststore/packages/core/discovery.config.default.js
@@ -1,5 +1,5 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.6 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.0-dev.8 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
@@ -7,19 +7,20 @@
7
7
 
8
8
  Not implemented: navigation to another Document
9
9
  Not implemented: navigation to another Document
10
- ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1180ms
10
+ ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1147ms
11
11
  Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
12
12
  npx update-browserslist-db@latest
13
13
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
14
- ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 2735ms
15
- ✓ shows the tab bar and renders Personal cards by default  986ms
16
- ✓ paginates when there are more than 25 cards  484ms
17
- ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2741ms
18
- ✓ shows an error state when the list fails to load  834ms
19
- ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  480ms
20
- ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 286ms
21
- ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 28ms
22
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 128ms
14
+ ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 1965ms
15
+ ✓ shows the tab bar and renders Personal cards by default  771ms
16
+ ✓ paginates when there are more than 25 cards  352ms
17
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2079ms
18
+ ✓ shows an error state when the list fails to load  553ms
19
+ ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  305ms
20
+ ✓  browser  test/sdk/localization/useBindingSelector.browser.test.tsx (9 tests) 106ms
21
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 33ms
22
+ ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 82ms
23
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 161ms
23
24
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
24
25
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
25
26
   at node:internal/deps/undici/undici:16416:13
@@ -49,7 +50,7 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
49
50
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
50
51
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
51
52
 
52
- ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 234ms
53
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 270ms
53
54
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > posts the password and redirects to the requested return path
54
55
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
55
56
 
@@ -135,7 +136,6 @@ act(() => {
135
136
  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
136
137
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
137
138
 
138
- ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 32ms
139
139
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > uses the fallback invalid-password message when API omits an error
140
140
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
141
141
 
@@ -184,8 +184,6 @@ act(() => {
184
184
  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
185
185
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
186
186
 
187
- ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1658ms
188
- ✓ renders the menu view with contract details and manage footer  988ms
189
187
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > shows service unavailable when the response is not a login payload
190
188
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
191
189
 
@@ -282,9 +280,12 @@ act(() => {
282
280
  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
283
281
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
284
282
 
285
- ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1773ms
286
- ✓ renders the password protection form  871ms
287
- ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 33ms
283
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1334ms
284
+ ✓ renders the password protection form  554ms
285
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 16ms
286
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1072ms
287
+ ✓ renders the menu view with contract details and manage footer  692ms
288
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 17ms
288
289
  stderr | test/sdk/localization/store-url.browser.test.ts
289
290
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
290
291
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -293,7 +294,7 @@ This ensures that you're testing the behavior the user would see in the browser.
293
294
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
294
295
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
295
296
 
296
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 6ms
297
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 5ms
297
298
  stderr | test/components/cms/RenderSections.browser.test.tsx
298
299
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
299
300
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -302,11 +303,11 @@ This ensures that you're testing the behavior the user would see in the browser.
302
303
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
303
304
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
304
305
 
305
- ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 91ms
306
- ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 34ms
307
- ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 43ms
308
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 129ms
309
- ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 43ms
306
+ ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 41ms
307
+ ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 77ms
308
+ ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 38ms
309
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 132ms
310
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 30ms
310
311
  stderr | test/components/account/doLogout.browser.test.ts
311
312
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
312
313
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -315,10 +316,11 @@ This ensures that you're testing the behavior the user would see in the browser.
315
316
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
316
317
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
317
318
 
318
- ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 29ms
319
- ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 98ms
320
- ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 328ms
321
- ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 313ms
319
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 38ms
320
+ ✓  browser  test/components/ui/LocalizationButton.browser.test.tsx (3 tests) 47ms
321
+ ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 78ms
322
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 278ms
323
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 296ms
322
324
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
323
325
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
324
326
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -327,10 +329,11 @@ This ensures that you're testing the behavior the user would see in the browser.
327
329
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
328
330
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
329
331
 
330
- ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 7ms
331
- ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 5ms
332
+ ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 8ms
333
+ ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 4ms
332
334
  ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 3ms
333
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 32ms
335
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 29ms
336
+ ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 29ms
334
337
  stderr | test/sdk/account/refreshToken.browser.test.ts
335
338
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
336
339
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -339,12 +342,11 @@ This ensures that you're testing the behavior the user would see in the browser.
339
342
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
340
343
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
341
344
 
342
- ✓  node  test/server/password-protection-service.test.ts (22 tests) 38ms
343
345
  ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 6ms
344
- ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 29ms
346
+ ✓  node  test/server/password-protection-service.test.ts (22 tests) 32ms
345
347
  ✓  node  test/utils/match-url.test.ts (10 tests) 9ms
346
- ✓  node  test/pages/api/unlock.test.ts (15 tests) 229ms
347
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 16ms
348
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 13ms
349
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 159ms
348
350
  ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 9ms
349
351
  stderr | test/components/search/SearchInputComponent.test.tsx
350
352
  Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
@@ -409,19 +411,19 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
409
411
  at Suspense
410
412
  at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
411
413
 
412
- ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 82ms
413
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 58ms
414
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 15ms
414
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 72ms
415
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 53ms
416
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 11ms
415
417
  stderr | test/pages/pvt/account/cards/index.test.ts > Cards page getServerSideProps > redirects to /pvt/account/403 when the query fails with 401/403
416
418
  { extensions: { status: 401 } }
417
419
 
418
420
  stderr | test/pages/pvt/account/cards/index.test.ts > Cards page getServerSideProps > renders the error state (no redirect) when the query fails with a non-auth status
419
421
  { extensions: { status: 502 } }
420
422
 
421
- ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 12ms
422
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 22ms
423
- ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 10ms
424
- ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 451ms
423
+ ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 8ms
424
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 21ms
425
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 9ms
426
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 385ms
425
427
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
426
428
  Graphql execution returned with error: [
427
429
  {
@@ -470,7 +472,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
470
472
 
471
473
  stderr | test/pages/api/graphql.test.ts > /api/graphql request handling > maps BadRequestError thrown by execute to its status
472
474
  Something unexpected occurred querying Graphql endpoint:
473
- E [FastStoreError]: malformed
475
+ T [FastStoreError]: malformed
474
476
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:190:31
475
477
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:196:39
476
478
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1024:60
@@ -514,68 +516,67 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
514
516
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37
515
517
  at Traces.$ (file:///home/runner/work/faststore/faststore/node_modules/.pnpm/vitest@4.0.18_@opentelemetry+api@1.9.0_@types+node@20.19.13_jiti@2.6.1_jsdom@27.4.0_sas_13be941c57d55f08f06eeaf1c180ddfe/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27)
516
518
 
517
- ✓  node  test/pages/api/graphql.test.ts (17 tests) 32ms
518
- ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 811ms
519
- ✓ navigates with the search term on blur  361ms
520
- ✓ navigates without a label param when the search is cleared  359ms
519
+ ✓  node  test/pages/api/graphql.test.ts (17 tests) 25ms
520
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 828ms
521
+ ✓ navigates with the search term on blur  362ms
522
+ ✓ navigates without a label param when the search is cleared  361ms
521
523
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
522
524
  Set password request failed: Bad Request
523
525
 
524
- ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 34ms
525
- ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 43ms
526
- ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 41ms
527
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 7ms
528
- ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 7ms
529
- ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 10ms
530
- ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 45ms
531
- ✓  node  test/sdk/redirects/index.test.ts (7 tests) 41ms
532
- ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 182ms
533
- ✓  node  test/utils/productJsonLd.test.ts (17 tests) 10ms
534
- ✓  node  test/proxy.localization.test.ts (5 tests) 56ms
535
- ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 46ms
536
- ✓  node  test/server/index.test.ts (7 tests) 1302ms
537
- ✓ should return a valid merged GraphQL schema  334ms
538
- ✓ should handle options and execute  689ms
526
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 45ms
527
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 39ms
528
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 37ms
529
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 6ms
530
+ ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 9ms
531
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 8ms
532
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 55ms
533
+ ✓  node  test/sdk/redirects/index.test.ts (7 tests) 59ms
534
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 148ms
535
+ ✓  node  test/utils/productJsonLd.test.ts (17 tests) 11ms
536
+ ✓  node  test/proxy.localization.test.ts (5 tests) 35ms
537
+ ✓  node  test/server/index.test.ts (7 tests) 846ms
538
+ ✓ should return a valid merged GraphQL schema  313ms
539
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 59ms
539
540
  ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 7ms
540
- ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 118ms
541
- ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 48ms
542
- ✓  node  test/server/webops-api.test.ts (9 tests) 16ms
543
- ✓  node  test/proxy.test.ts (4 tests) 17ms
544
- ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 12ms
545
- ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 8ms
546
- ✓  node  test/server/content/service.test.ts (5 tests) 8ms
547
- ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 45ms
548
- ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 8ms
549
- ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 16ms
550
- ✓  node  test/server/cms/global.test.ts (3 tests) 7ms
551
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 5ms
552
- ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 31ms
553
- ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 163ms
541
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 88ms
542
+ ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 42ms
543
+ ✓  node  test/proxy.test.ts (4 tests) 20ms
544
+ ✓  node  test/server/webops-api.test.ts (9 tests) 18ms
545
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 36ms
546
+ ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 10ms
547
+ ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 5ms
548
+ ✓  node  test/server/content/service.test.ts (5 tests) 5ms
549
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 7ms
550
+ ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 12ms
551
+ ✓  node  test/server/cms/global.test.ts (3 tests) 6ms
552
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 4ms
553
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 34ms
554
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 127ms
554
555
  ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 7ms
555
- ✓  node  test/pages/api/preview.test.ts (2 tests) 10ms
556
- ✓  node  test/server/rootSpan.test.ts (4 tests) 49ms
557
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 11ms
558
- ✓  node  test/components/search/SearchInput.test.ts (6 tests) 6ms
559
- ✓  node  test/components/account/Layout.test.tsx (6 tests) 76ms
560
- ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 7ms
561
- ✓  node  test/utils/retry.test.ts (5 tests) 15ms
562
- ✓  node  test/server/cms/index.test.ts (2 tests) 6ms
556
+ ✓  node  test/server/rootSpan.test.ts (4 tests) 41ms
557
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 8ms
558
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 5ms
559
+ ✓  node  test/components/account/Layout.test.tsx (6 tests) 61ms
560
+ ✓  node  test/components/search/SearchInput.test.ts (6 tests) 8ms
561
+ ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 6ms
562
+ ✓  node  test/utils/retry.test.ts (5 tests) 14ms
563
+ ✓  node  test/server/cms/index.test.ts (2 tests) 5ms
563
564
  ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 tests) 5ms
564
- ✓  node  test/utils/unlockResponse.test.ts (11 tests) 5ms
565
- ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 6ms
566
- ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 192ms
567
- ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 52ms
568
- ✓  node  test/utils/isLocalHost.test.ts (7 tests) 6ms
569
- ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 5ms
565
+ ✓  node  test/utils/unlockResponse.test.ts (11 tests) 7ms
566
+ ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 3ms
567
+ ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 185ms
568
+ ✓  node  test/utils/isLocalHost.test.ts (7 tests) 5ms
569
+ ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 4ms
570
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 44ms
570
571
  ✓  node  test/utils/getCookie.test.ts (4 tests) 5ms
571
- ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 44ms
572
- ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 5ms
573
- ✓  node  test/instrumentation.test.ts (3 tests) 7ms
574
- ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 28ms
575
- ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 16ms
576
-
577
-  Test Files  91 passed (91)
578
-  Tests  730 passed (730)
579
-  Start at  17:35:39
580
-  Duration  45.76s (transform 13.02s, setup 0ms, import 41.25s, tests 16.71s, environment 55.14s)
572
+ ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 4ms
573
+ ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 32ms
574
+ ✓  node  test/instrumentation.test.ts (3 tests) 6ms
575
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 11ms
576
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 24ms
577
+
578
+  Test Files  93 passed (93)
579
+  Tests  742 passed (742)
580
+  Start at  13:03:37
581
+  Duration  44.11s (transform 12.29s, setup 0ms, import 41.01s, tests 13.32s, environment 54.59s)
581
582
 
package/@generated/gql.ts CHANGED
@@ -30,7 +30,7 @@ type Documents = {
30
30
  "\n fragment ServerProduct on Query {\n product(locator: $locator) {\n id: productID\n }\n }\n": typeof types.ServerProductFragmentDoc,
31
31
  "\n query ServerAccountPageQuery {\n accountProfile {\n name\n }\n }\n": typeof types.ServerAccountPageQueryDocument,
32
32
  "\n query ServerCollectionPageQuery($slug: String!) {\n ...ServerCollectionPage\n collection(slug: $slug) {\n seo {\n title\n description\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n meta {\n selectedFacets {\n key\n value\n }\n }\n otherLocales {\n locale\n slug\n }\n }\n }\n": typeof types.ServerCollectionPageQueryDocument,
33
- "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n ...ProductDetailsFragment_product\n }\n }\n": typeof types.ServerProductQueryDocument,
33
+ "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n defaultLocaleSlug\n\n ...ProductDetailsFragment_product\n }\n }\n": typeof types.ServerProductQueryDocument,
34
34
  "\n query ServerListCardsQuery {\n listCreditCards {\n list {\n accountId\n bin\n cardNumber\n cardLabel\n paymentSystem\n paymentSystemName\n isDefault\n isActive\n origin\n }\n }\n accountProfile {\n name\n }\n hasAdHocCardAccess\n }\n": typeof types.ServerListCardsQueryDocument,
35
35
  "\n fragment UserOrderItemsFragment on UserOrderItems {\n id\n name\n quantity\n sellingPrice\n unitMultiplier\n measurementUnit\n imageUrl\n detailUrl\n refId\n rewardValue\n }\n": typeof types.UserOrderItemsFragmentFragmentDoc,
36
36
  "\n query ServerOrderDetailsQuery($orderId: String!) {\n userOrder(orderId: $orderId) {\n orderId\n creationDate\n status\n canProcessOrderAuthorization\n statusDescription\n allowCancellation\n ruleForAuthorization {\n orderAuthorizationId\n dimensionId\n rule {\n id\n name\n status\n doId\n authorizedEmails\n priority\n trigger {\n condition {\n conditionType\n description\n lessThan\n greatherThan\n expression\n }\n effect {\n description\n effectType\n funcPath\n }\n }\n timeout\n notification\n scoreInterval {\n accept\n deny\n }\n authorizationData {\n requireAllApprovals\n authorizers {\n id\n email\n type\n authorizationDate\n }\n }\n isUserAuthorized\n isUserNextAuthorizer\n }\n }\n storePreferencesData {\n currencyCode\n }\n clientProfileData {\n firstName\n lastName\n email\n phone\n corporateName\n isCorporate\n }\n customFields {\n type\n id\n fields {\n name\n value\n refId\n }\n }\n deliveryOptionsData {\n deliveryOptions {\n selectedSla\n deliveryChannel\n deliveryCompany\n deliveryWindow {\n startDateUtc\n endDateUtc\n price\n }\n shippingEstimate\n shippingEstimateDate\n friendlyShippingEstimate\n friendlyDeliveryOptionName\n seller\n address {\n addressType\n receiverName\n addressId\n versionId\n entityId\n postalCode\n city\n state\n country\n street\n number\n neighborhood\n complement\n reference\n geoCoordinates\n }\n pickupStoreInfo {\n additionalInfo\n address {\n addressType\n receiverName\n addressId\n versionId\n entityId\n postalCode\n city\n state\n country\n street\n number\n neighborhood\n complement\n reference\n geoCoordinates\n }\n dockId\n friendlyName\n isPickupStore\n }\n quantityOfDifferentItems\n total\n items {\n id\n uniqueId\n name\n quantity\n price\n sellingPrice\n imageUrl\n tax\n taxPriceTagsTotal\n total\n }\n }\n contact {\n email\n phone\n name\n }\n }\n paymentData {\n transactions {\n isActive\n payments {\n id\n paymentSystemName\n value\n installments\n referenceValue\n lastDigits\n url\n group\n tid\n connectorResponses {\n authId\n }\n bankIssuedInvoiceIdentificationNumber\n redemptionCode\n paymentOrigin\n }\n }\n }\n totals {\n id\n name\n value\n }\n shopper {\n firstName\n lastName\n email\n phone\n }\n budgetData {\n budgets {\n id\n name\n balance {\n remaining\n }\n allocations {\n id\n linkedEntity {\n id\n }\n reservations\n }\n }\n }\n }\n accountProfile {\n name\n }\n }\n": typeof types.ServerOrderDetailsQueryDocument,
@@ -83,7 +83,7 @@ const documents: Documents = {
83
83
  "\n fragment ServerProduct on Query {\n product(locator: $locator) {\n id: productID\n }\n }\n": types.ServerProductFragmentDoc,
84
84
  "\n query ServerAccountPageQuery {\n accountProfile {\n name\n }\n }\n": types.ServerAccountPageQueryDocument,
85
85
  "\n query ServerCollectionPageQuery($slug: String!) {\n ...ServerCollectionPage\n collection(slug: $slug) {\n seo {\n title\n description\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n meta {\n selectedFacets {\n key\n value\n }\n }\n otherLocales {\n locale\n slug\n }\n }\n }\n": types.ServerCollectionPageQueryDocument,
86
- "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n ...ProductDetailsFragment_product\n }\n }\n": types.ServerProductQueryDocument,
86
+ "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n defaultLocaleSlug\n\n ...ProductDetailsFragment_product\n }\n }\n": types.ServerProductQueryDocument,
87
87
  "\n query ServerListCardsQuery {\n listCreditCards {\n list {\n accountId\n bin\n cardNumber\n cardLabel\n paymentSystem\n paymentSystemName\n isDefault\n isActive\n origin\n }\n }\n accountProfile {\n name\n }\n hasAdHocCardAccess\n }\n": types.ServerListCardsQueryDocument,
88
88
  "\n fragment UserOrderItemsFragment on UserOrderItems {\n id\n name\n quantity\n sellingPrice\n unitMultiplier\n measurementUnit\n imageUrl\n detailUrl\n refId\n rewardValue\n }\n": types.UserOrderItemsFragmentFragmentDoc,
89
89
  "\n query ServerOrderDetailsQuery($orderId: String!) {\n userOrder(orderId: $orderId) {\n orderId\n creationDate\n status\n canProcessOrderAuthorization\n statusDescription\n allowCancellation\n ruleForAuthorization {\n orderAuthorizationId\n dimensionId\n rule {\n id\n name\n status\n doId\n authorizedEmails\n priority\n trigger {\n condition {\n conditionType\n description\n lessThan\n greatherThan\n expression\n }\n effect {\n description\n effectType\n funcPath\n }\n }\n timeout\n notification\n scoreInterval {\n accept\n deny\n }\n authorizationData {\n requireAllApprovals\n authorizers {\n id\n email\n type\n authorizationDate\n }\n }\n isUserAuthorized\n isUserNextAuthorizer\n }\n }\n storePreferencesData {\n currencyCode\n }\n clientProfileData {\n firstName\n lastName\n email\n phone\n corporateName\n isCorporate\n }\n customFields {\n type\n id\n fields {\n name\n value\n refId\n }\n }\n deliveryOptionsData {\n deliveryOptions {\n selectedSla\n deliveryChannel\n deliveryCompany\n deliveryWindow {\n startDateUtc\n endDateUtc\n price\n }\n shippingEstimate\n shippingEstimateDate\n friendlyShippingEstimate\n friendlyDeliveryOptionName\n seller\n address {\n addressType\n receiverName\n addressId\n versionId\n entityId\n postalCode\n city\n state\n country\n street\n number\n neighborhood\n complement\n reference\n geoCoordinates\n }\n pickupStoreInfo {\n additionalInfo\n address {\n addressType\n receiverName\n addressId\n versionId\n entityId\n postalCode\n city\n state\n country\n street\n number\n neighborhood\n complement\n reference\n geoCoordinates\n }\n dockId\n friendlyName\n isPickupStore\n }\n quantityOfDifferentItems\n total\n items {\n id\n uniqueId\n name\n quantity\n price\n sellingPrice\n imageUrl\n tax\n taxPriceTagsTotal\n total\n }\n }\n contact {\n email\n phone\n name\n }\n }\n paymentData {\n transactions {\n isActive\n payments {\n id\n paymentSystemName\n value\n installments\n referenceValue\n lastDigits\n url\n group\n tid\n connectorResponses {\n authId\n }\n bankIssuedInvoiceIdentificationNumber\n redemptionCode\n paymentOrigin\n }\n }\n }\n totals {\n id\n name\n value\n }\n shopper {\n firstName\n lastName\n email\n phone\n }\n budgetData {\n budgets {\n id\n name\n balance {\n remaining\n }\n allocations {\n id\n linkedEntity {\n id\n }\n reservations\n }\n }\n }\n }\n accountProfile {\n name\n }\n }\n": types.ServerOrderDetailsQueryDocument,
@@ -184,7 +184,7 @@ export function gql(source: "\n query ServerCollectionPageQuery($slug: String!)
184
184
  /**
185
185
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
186
186
  */
187
- export function gql(source: "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n ...ProductDetailsFragment_product\n }\n }\n"): typeof import('./graphql').ServerProductQueryDocument;
187
+ export function gql(source: "\n query ServerProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ServerProduct\n product(locator: $locator) {\n id: productID\n\n seo {\n title\n description\n canonical\n }\n\n brand {\n name\n }\n\n sku\n gtin\n mpn\n name\n description\n releaseDate\n\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n\n image {\n url\n alternateName\n }\n\n offers {\n lowPrice\n highPrice\n lowPriceWithTaxes\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n priceToken\n seller {\n identifier\n }\n }\n }\n\n isVariantOf {\n productGroupID\n }\n\n otherLocales {\n locale\n slug\n }\n\n defaultLocaleSlug\n\n ...ProductDetailsFragment_product\n }\n }\n"): typeof import('./graphql').ServerProductQueryDocument;
188
188
  /**
189
189
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
190
190
  */
@@ -1681,6 +1681,14 @@ export type StoreProduct = {
1681
1681
  brand: StoreBrand;
1682
1682
  /** List of items consisting of chain linked web pages, ending with the current page. */
1683
1683
  breadcrumbList: StoreBreadcrumbList;
1684
+ /**
1685
+ * Slug for the store's default locale, including the SKU ID suffix.
1686
+ * Navigation fallback for the locale selector when the target locale is absent
1687
+ * from `otherLocales`. Best-effort: it may point at a slug the catalog has not
1688
+ * registered for the default locale, so it must never be used to build hreflang
1689
+ * annotations. Only populated when localization is enabled.
1690
+ */
1691
+ defaultLocaleSlug: Maybe<Scalars['String']['output']>;
1684
1692
  /** Delivery Promise product's badge. */
1685
1693
  deliveryPromiseBadges: Maybe<Array<Maybe<DeliveryPromiseBadge>>>;
1686
1694
  /** Product description. */
@@ -2819,7 +2827,7 @@ export type ServerProductQueryQueryVariables = Exact<{
2819
2827
  }>;
2820
2828
 
2821
2829
 
2822
- export type ServerProductQueryQuery = { product: { sku: string, gtin: string, mpn: string, name: string, description: string, releaseDate: string, unitMultiplier: number | null, id: string, seo: { title: string, description: string, canonical: string }, brand: { name: string }, breadcrumbList: { itemListElement: Array<{ item: string, name: string, position: number }> }, image: Array<{ url: string, alternateName: string }>, offers: { lowPrice: number, highPrice: number, lowPriceWithTaxes: number, priceCurrency: string, offers: Array<{ availability: string, price: number, priceValidUntil: string, priceCurrency: string, itemCondition: string, priceToken: string | null, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, quantity: number, seller: { identifier: string } }> }, isVariantOf: { name: string, productGroupID: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, otherLocales: Array<{ locale: string, slug: string }> | null, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } };
2830
+ export type ServerProductQueryQuery = { product: { sku: string, gtin: string, mpn: string, name: string, description: string, releaseDate: string, defaultLocaleSlug: string | null, unitMultiplier: number | null, id: string, seo: { title: string, description: string, canonical: string }, brand: { name: string }, breadcrumbList: { itemListElement: Array<{ item: string, name: string, position: number }> }, image: Array<{ url: string, alternateName: string }>, offers: { lowPrice: number, highPrice: number, lowPriceWithTaxes: number, priceCurrency: string, offers: Array<{ availability: string, price: number, priceValidUntil: string, priceCurrency: string, itemCondition: string, priceToken: string | null, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, quantity: number, seller: { identifier: string } }> }, isVariantOf: { name: string, productGroupID: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, otherLocales: Array<{ locale: string, slug: string }> | null, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } };
2823
2831
 
2824
2832
  export type ServerListCardsQueryQueryVariables = Exact<{ [key: string]: never; }>;
2825
2833
 
@@ -3617,7 +3625,7 @@ export const SearchEvent_MetadataFragmentDoc = new TypedDocumentString(`
3617
3625
  `, {"fragmentName":"SearchEvent_metadata"}) as unknown as TypedDocumentString<SearchEvent_MetadataFragment, unknown>;
3618
3626
  export const ServerAccountPageQueryDocument = {"__meta__":{"operationName":"ServerAccountPageQuery","operationHash":"9baae331b75848a310fecb457e8c971ae27897ff"}} as unknown as TypedDocumentString<ServerAccountPageQueryQuery, ServerAccountPageQueryQueryVariables>;
3619
3627
  export const ServerCollectionPageQueryDocument = {"__meta__":{"operationName":"ServerCollectionPageQuery","operationHash":"d46841b30ae1f6350021b5cf02f253d56c848664"}} as unknown as TypedDocumentString<ServerCollectionPageQueryQuery, ServerCollectionPageQueryQueryVariables>;
3620
- export const ServerProductQueryDocument = {"__meta__":{"operationName":"ServerProductQuery","operationHash":"b89e93519be01aebc01c402489a0ae640d38675a"}} as unknown as TypedDocumentString<ServerProductQueryQuery, ServerProductQueryQueryVariables>;
3628
+ export const ServerProductQueryDocument = {"__meta__":{"operationName":"ServerProductQuery","operationHash":"13fc9efa8628b91971e3a8aa985e6cdbed70adae"}} as unknown as TypedDocumentString<ServerProductQueryQuery, ServerProductQueryQueryVariables>;
3621
3629
  export const ServerListCardsQueryDocument = {"__meta__":{"operationName":"ServerListCardsQuery","operationHash":"392cf85d18d66b94d6ea8d6b2a3c6ffb5c94683d"}} as unknown as TypedDocumentString<ServerListCardsQueryQuery, ServerListCardsQueryQueryVariables>;
3622
3630
  export const ServerOrderDetailsQueryDocument = {"__meta__":{"operationName":"ServerOrderDetailsQuery","operationHash":"bdf677bbccce12186a5ef15aebdce46585a99782"}} as unknown as TypedDocumentString<ServerOrderDetailsQueryQuery, ServerOrderDetailsQueryQueryVariables>;
3623
3631
  export const ServerListOrdersQueryDocument = {"__meta__":{"operationName":"ServerListOrdersQuery","operationHash":"70d06de1da9c11f10ebde31b66fd74eccd456af5"}} as unknown as TypedDocumentString<ServerListOrdersQueryQuery, ServerListOrdersQueryQueryVariables>;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "9baae331b75848a310fecb457e8c971ae27897ff": "query ServerAccountPageQuery { accountProfile { name } }",
3
3
  "d46841b30ae1f6350021b5cf02f253d56c848664": "fragment ServerCollectionPage on Query { collection(slug: $slug) { id } } query ServerCollectionPageQuery($slug: String!) { collection(slug: $slug) { breadcrumbList { itemListElement { item name position } } meta { selectedFacets { key value } } otherLocales { locale slug } seo { description title } } ...ServerCollectionPage }",
4
- "b89e93519be01aebc01c402489a0ae640d38675a": "fragment CartProductItem on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations availableVariations slugsMap } } name sku unitMultiplier } fragment ProductDetailsFragment_product on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } description gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku unitMultiplier ...CartProductItem } fragment ServerProduct on Query { product(locator: $locator) { id: productID } } query ServerProductQuery($locator: [IStoreSelectedFacet!]!) { product(locator: $locator) { brand { name } breadcrumbList { itemListElement { item name position } } description gtin image { alternateName url } isVariantOf { productGroupID } mpn name offers { highPrice lowPrice lowPriceWithTaxes offers { availability itemCondition price priceCurrency priceToken priceValidUntil seller { identifier } } priceCurrency } otherLocales { locale slug } id: productID releaseDate seo { canonical description title } sku ...ProductDetailsFragment_product } ...ServerProduct }",
4
+ "13fc9efa8628b91971e3a8aa985e6cdbed70adae": "fragment CartProductItem on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations availableVariations slugsMap } } name sku unitMultiplier } fragment ProductDetailsFragment_product on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } description gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku unitMultiplier ...CartProductItem } fragment ServerProduct on Query { product(locator: $locator) { id: productID } } query ServerProductQuery($locator: [IStoreSelectedFacet!]!) { product(locator: $locator) { brand { name } breadcrumbList { itemListElement { item name position } } defaultLocaleSlug description gtin image { alternateName url } isVariantOf { productGroupID } mpn name offers { highPrice lowPrice lowPriceWithTaxes offers { availability itemCondition price priceCurrency priceToken priceValidUntil seller { identifier } } priceCurrency } otherLocales { locale slug } id: productID releaseDate seo { canonical description title } sku ...ProductDetailsFragment_product } ...ServerProduct }",
5
5
  "392cf85d18d66b94d6ea8d6b2a3c6ffb5c94683d": "query ServerListCardsQuery { accountProfile { name } hasAdHocCardAccess listCreditCards { list { accountId bin cardLabel cardNumber isActive isDefault origin paymentSystem paymentSystemName } } }",
6
6
  "bdf677bbccce12186a5ef15aebdce46585a99782": "query ServerOrderDetailsQuery($orderId: String!) { accountProfile { name } userOrder(orderId: $orderId) { allowCancellation budgetData { budgets { allocations { id linkedEntity { id } reservations } balance { remaining } id name } } canProcessOrderAuthorization clientProfileData { corporateName email firstName isCorporate lastName phone } creationDate customFields { fields { name refId value } id type } deliveryOptionsData { contact { email name phone } deliveryOptions { address { addressId addressType city complement country entityId geoCoordinates neighborhood number postalCode receiverName reference state street versionId } deliveryChannel deliveryCompany deliveryWindow { endDateUtc price startDateUtc } friendlyDeliveryOptionName friendlyShippingEstimate items { id imageUrl name price quantity sellingPrice tax taxPriceTagsTotal total uniqueId } pickupStoreInfo { additionalInfo address { addressId addressType city complement country entityId geoCoordinates neighborhood number postalCode receiverName reference state street versionId } dockId friendlyName isPickupStore } quantityOfDifferentItems selectedSla seller shippingEstimate shippingEstimateDate total } } orderId paymentData { transactions { isActive payments { bankIssuedInvoiceIdentificationNumber connectorResponses { authId } group id installments lastDigits paymentOrigin paymentSystemName redemptionCode referenceValue tid url value } } } ruleForAuthorization { dimensionId orderAuthorizationId rule { authorizationData { authorizers { authorizationDate email id type } requireAllApprovals } authorizedEmails doId id isUserAuthorized isUserNextAuthorizer name notification priority scoreInterval { accept deny } status timeout trigger { condition { conditionType description expression greatherThan lessThan } effect { description effectType funcPath } } } } shopper { email firstName lastName phone } status statusDescription storePreferencesData { currencyCode } totals { id name value } } }",
7
7
  "70d06de1da9c11f10ebde31b66fd74eccd456af5": "query ServerListOrdersQuery($clientEmail: String, $dateFinal: String, $dateInitial: String, $page: Int, $pendingMyApproval: Boolean, $perPage: Int, $status: [String], $text: String) { accountProfile { name } listUserOrders( page: $page perPage: $perPage status: $status dateInitial: $dateInitial dateFinal: $dateFinal text: $text clientEmail: $clientEmail pendingMyApproval: $pendingMyApproval ) { list { ShippingEstimatedDate clientName creationDate currencyCode customFields { type value } items { description ean id price productId quantity refId seller sellingPrice } orderId status statusDescription totalValue } paging { currentPage pages perPage total } } }",
@@ -1735,6 +1735,14 @@ type StoreProduct {
1735
1735
  Only populated when localization is enabled.
1736
1736
  """
1737
1737
  otherLocales: [StoreProductLocale!]
1738
+ """
1739
+ Slug for the store's default locale, including the SKU ID suffix.
1740
+ Navigation fallback for the locale selector when the target locale is absent
1741
+ from `otherLocales`. Best-effort: it may point at a slug the catalog has not
1742
+ registered for the default locale, so it must never be used to build hreflang
1743
+ annotations. Only populated when localization is enabled.
1744
+ """
1745
+ defaultLocaleSlug: String
1738
1746
  }
1739
1747
 
1740
1748
  """
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.7.0-dev.9](https://github.com/vtex/faststore/compare/v4.7.0-dev.8...v4.7.0-dev.9) (2026-08-31)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **api:** separate hreflang slugs from locale-switch navigation ([#3462](https://github.com/vtex/faststore/issues/3462)) ([3091a46](https://github.com/vtex/faststore/commit/3091a46031f387d1e11e5747dd0f4ac6bef358e2))
11
+
12
+ # [4.7.0-dev.8](https://github.com/vtex/faststore/compare/v4.7.0-dev.7...v4.7.0-dev.8) (2026-08-28)
13
+
14
+ **Note:** Version bump only for package @faststore/core
15
+
6
16
  # [4.7.0-dev.7](https://github.com/vtex/faststore/compare/v4.7.0-dev.6...v4.7.0-dev.7) (2026-08-28)
7
17
 
8
18
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.7.0-dev.7",
3
+ "version": "4.7.0-dev.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -71,11 +71,11 @@
71
71
  "style-loader": "^3.3.1",
72
72
  "swr": "^2.2.5",
73
73
  "use-sync-external-store": "^1.6.0",
74
- "@faststore/api": "4.7.0-dev.7",
75
- "@faststore/diagnostics": "4.7.0-dev.7",
76
- "@faststore/lighthouse": "4.7.0-dev.7",
77
- "@faststore/sdk": "4.7.0-dev.7",
78
- "@faststore/ui": "4.7.0-dev.7"
74
+ "@faststore/api": "4.7.0-dev.9",
75
+ "@faststore/lighthouse": "4.7.0-dev.9",
76
+ "@faststore/sdk": "4.7.0-dev.9",
77
+ "@faststore/ui": "4.7.0-dev.9",
78
+ "@faststore/diagnostics": "4.7.0-dev.9"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@cypress/code-coverage": "^3.12.1",
@@ -42,8 +42,10 @@ const LocalizationButton = ({
42
42
  const [isSelectorOpen, setIsSelectorOpen] = useState(false)
43
43
  const buttonRef = useRef<HTMLButtonElement>(null)
44
44
 
45
- const otherLocales = useLocalizedProduct()?.otherLocales ?? undefined
46
- const urlSuffix = useLocalizedProduct()?.urlSuffix ?? '/p'
45
+ const localizedProduct = useLocalizedProduct()
46
+ const otherLocales = localizedProduct?.otherLocales ?? undefined
47
+ const urlSuffix = localizedProduct?.urlSuffix ?? '/p'
48
+ const defaultLocaleSlug = localizedProduct?.defaultLocaleSlug ?? undefined
47
49
 
48
50
  const {
49
51
  languages,
@@ -56,7 +58,7 @@ const LocalizationButton = ({
56
58
  reset,
57
59
  isSaveEnabled,
58
60
  error,
59
- } = useBindingSelector(otherLocales, urlSuffix)
61
+ } = useBindingSelector(otherLocales, urlSuffix, defaultLocaleSlug)
60
62
 
61
63
  const { locale: sessionLocale, currency: sessionCurrency } = useSession()
62
64
 
@@ -352,7 +352,10 @@ function Page({
352
352
  If needed, wrap your component in a <Section /> component
353
353
  (not the HTML tag) before rendering it here.
354
354
  */}
355
- <LocalizedProductProvider otherLocales={server.product.otherLocales}>
355
+ <LocalizedProductProvider
356
+ otherLocales={server.product.otherLocales}
357
+ defaultLocaleSlug={server.product.defaultLocaleSlug}
358
+ >
356
359
  <PageProvider context={context}>
357
360
  <RenderSections
358
361
  sections={sections}
@@ -428,6 +431,8 @@ const query = gql(`
428
431
  slug
429
432
  }
430
433
 
434
+ defaultLocaleSlug
435
+
431
436
  ...ProductDetailsFragment_product
432
437
  }
433
438
  }
@@ -12,6 +12,12 @@ interface LocalizedProductData {
12
12
  * Null when localization is disabled or the page type does not support it.
13
13
  */
14
14
  otherLocales: LocalizedProductLocale[] | null
15
+ /**
16
+ * Slug for the store's default locale, used to navigate to a locale that is
17
+ * absent from `otherLocales`. Null outside PDPs, where the API does not
18
+ * provide one.
19
+ */
20
+ defaultLocaleSlug: string | null
15
21
  /**
16
22
  * Suffix appended after the localized slug when building the redirect URL.
17
23
  * Use '/p' for product pages (PDP), '' for collection/PLP pages.
@@ -24,6 +30,7 @@ const LocalizedProductContext = createContext<LocalizedProductData | null>(null)
24
30
  type LocalizedProductProviderProps = Readonly<
25
31
  PropsWithChildren<{
26
32
  otherLocales: LocalizedProductLocale[] | null | undefined
33
+ defaultLocaleSlug?: string | null
27
34
  /**
28
35
  * Suffix to append to the localized slug when building the redirect URL.
29
36
  * Defaults to '/p' (product pages). Pass '' for collection/PLP pages.
@@ -41,12 +48,17 @@ type LocalizedProductProviderProps = Readonly<
41
48
  */
42
49
  export function LocalizedProductProvider({
43
50
  otherLocales,
51
+ defaultLocaleSlug,
44
52
  urlSuffix = '/p',
45
53
  children,
46
54
  }: LocalizedProductProviderProps) {
47
55
  const value = useMemo(
48
- () => ({ otherLocales: otherLocales ?? null, urlSuffix }),
49
- [otherLocales, urlSuffix]
56
+ () => ({
57
+ otherLocales: otherLocales ?? null,
58
+ defaultLocaleSlug: defaultLocaleSlug ?? null,
59
+ urlSuffix,
60
+ }),
61
+ [otherLocales, defaultLocaleSlug, urlSuffix]
50
62
  )
51
63
 
52
64
  return (
@@ -1,3 +1,4 @@
1
+ import type { LocalizedProductLocale } from './LocalizedProductContext'
1
2
  import type { Binding, Locale } from './types'
2
3
 
3
4
  /**
@@ -74,6 +75,43 @@ export function resolveBinding(
74
75
  return matches.find((b) => b.isDefault) ?? matches[0]
75
76
  }
76
77
 
78
+ /**
79
+ * Resolves which product slug the locale selector should navigate to.
80
+ *
81
+ * Falls back to the default locale's slug rather than to any other entry, so a
82
+ * translated slug is never carried across locales (e.g. an Italian slug served
83
+ * under an es-ES prefix).
84
+ *
85
+ * `otherLocales` only carries locales the catalog has a registered slug for, so
86
+ * an untranslated product has no entry at all — not even for the default
87
+ * locale. `defaultLocaleSlug` covers that gap: the API resolves it best-effort
88
+ * so navigation lands on the product page instead of the locale root.
89
+ *
90
+ * @param otherLocales - Registered localized slugs for the current product
91
+ * @param targetLocale - Locale the shopper is switching to
92
+ * @param defaultLocale - The store's default locale
93
+ * @param defaultLocaleSlug - Best-effort default-locale slug, PDP only
94
+ * @returns The slug to redirect to, or null when none is available
95
+ */
96
+ export function resolveTargetSlug({
97
+ otherLocales,
98
+ targetLocale,
99
+ defaultLocale,
100
+ defaultLocaleSlug,
101
+ }: {
102
+ otherLocales: LocalizedProductLocale[] | null | undefined
103
+ targetLocale: string
104
+ defaultLocale: string
105
+ defaultLocaleSlug?: string | null
106
+ }): string | null {
107
+ return (
108
+ otherLocales?.find((e) => e.locale === targetLocale)?.slug ??
109
+ otherLocales?.find((e) => e.locale === defaultLocale)?.slug ??
110
+ defaultLocaleSlug ??
111
+ null
112
+ )
113
+ }
114
+
77
115
  /**
78
116
  * Validates that a URL is non-empty and appears to be valid.
79
117
  *
@@ -8,6 +8,7 @@ import {
8
8
  getCurrenciesForLocale,
9
9
  isValidUrl,
10
10
  resolveBinding,
11
+ resolveTargetSlug,
11
12
  } from './bindingSelector'
12
13
  import type { LocalizedProductLocale } from './LocalizedProductContext'
13
14
  import type { BindingSelectorError, Locale } from './types'
@@ -147,11 +148,14 @@ export interface UseBindingSelectorReturn {
147
148
  * localized page URL instead of preserving the current page path verbatim.
148
149
  * @param urlSuffix - Suffix appended after the slug when building the redirect URL.
149
150
  * Use '/p' for product pages (default) and '' for collection/PLP pages.
151
+ * @param defaultLocaleSlug - Slug for the store's default locale, used when the
152
+ * target locale is absent from `otherLocales`. PDP only; PLPs pass nothing.
150
153
  * @returns Object with languages, currencies, selections, and actions
151
154
  */
152
155
  export function useBindingSelector(
153
156
  otherLocales?: Array<{ locale: string; slug: string }> | null,
154
- urlSuffix = '/p'
157
+ urlSuffix = '/p',
158
+ defaultLocaleSlug?: string | null
155
159
  ): UseBindingSelectorReturn {
156
160
  const { locale: currentLocale, currency: currentCurrency } = useSession()
157
161
  const localizationConfig = storeConfig.localization as LocalizationConfig
@@ -284,27 +288,17 @@ export function useBindingSelector(
284
288
  ? otherLocales
285
289
  : recoverOtherLocales()
286
290
 
287
- if (effectiveOtherLocales?.length) {
288
- // 1. Target locale has a specific translation → use it
289
- const localizedEntry = effectiveOtherLocales.find(
290
- (e) => e.locale === localeCode
291
- )
292
-
293
- // 2. No translation for target locale → fall back to the default locale slug
294
- // (IS linkText, always in the default locale) to avoid carrying over a
295
- // translated slug from a different locale (e.g. Italian slug on es-ES).
296
- // For an unavailable target this yields a 404 at the product URL (expected).
297
- const fallbackEntry = effectiveOtherLocales.find(
298
- (e) => e.locale === localizationConfig.defaultLocale
299
- )
300
-
301
- const entry = localizedEntry ?? fallbackEntry
302
-
303
- if (entry) {
304
- const baseUrl = binding.url.replace(/\/$/, '')
305
- globalThis.location.href = `${baseUrl}/${entry.slug}${urlSuffix}${globalThis.location.search}${globalThis.location.hash}`
306
- return
307
- }
291
+ const slug = resolveTargetSlug({
292
+ otherLocales: effectiveOtherLocales,
293
+ targetLocale: localeCode,
294
+ defaultLocale: localizationConfig.defaultLocale,
295
+ defaultLocaleSlug,
296
+ })
297
+
298
+ if (slug) {
299
+ const baseUrl = binding.url.replace(/\/$/, '')
300
+ globalThis.location.href = `${baseUrl}/${slug}${urlSuffix}${globalThis.location.search}${globalThis.location.hash}`
301
+ return
308
302
  }
309
303
 
310
304
  // otherLocales is empty/null but we're still on a PDP: strip the stale slug.
@@ -329,6 +323,7 @@ export function useBindingSelector(
329
323
  localizationConfig.defaultLocale,
330
324
  otherLocales,
331
325
  urlSuffix,
326
+ defaultLocaleSlug,
332
327
  ])
333
328
 
334
329
  const isSaveEnabled = Boolean(localeCode && currencyCode && !error)
@@ -0,0 +1,87 @@
1
+ import { cleanup, render } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ const mockUseBindingSelector = vi.hoisted(() =>
5
+ vi.fn(() => ({
6
+ languages: { 'en-US': 'English' },
7
+ currencies: { USD: 'USD' },
8
+ localeCode: 'en-US',
9
+ currencyCode: 'USD',
10
+ setLocaleCode: vi.fn(),
11
+ setCurrencyCode: vi.fn(),
12
+ save: vi.fn(),
13
+ reset: vi.fn(),
14
+ isSaveEnabled: true,
15
+ error: null,
16
+ }))
17
+ )
18
+
19
+ vi.mock('src/sdk/localization', () => ({
20
+ useBindingSelector: mockUseBindingSelector,
21
+ }))
22
+
23
+ vi.mock('src/sdk/session', () => ({
24
+ useSession: () => ({ locale: 'pt-BR', currency: { code: 'BRL' } }),
25
+ }))
26
+
27
+ vi.mock('discovery.config', async (importOriginal) => {
28
+ const original = await importOriginal<{ default: Record<string, unknown> }>()
29
+
30
+ return { default: { ...original.default, localization: { enabled: true } } }
31
+ })
32
+
33
+ import LocalizationButton from 'src/components/ui/LocalizationButton'
34
+ import { LocalizedProductProvider } from 'src/sdk/localization/LocalizedProductContext'
35
+
36
+ const OTHER_LOCALES = [
37
+ { locale: 'en-US', slug: 'roshe-tenis-76' },
38
+ { locale: 'pt-BR', slug: 'tenis-roshe-76' },
39
+ ]
40
+
41
+ afterEach(() => {
42
+ cleanup()
43
+ mockUseBindingSelector.mockClear()
44
+ })
45
+
46
+ describe('LocalizationButton', () => {
47
+ it('forwards both slug sources from the product context to the selector', () => {
48
+ render(
49
+ <LocalizedProductProvider
50
+ otherLocales={OTHER_LOCALES}
51
+ defaultLocaleSlug="roshe-tenis-76"
52
+ >
53
+ <LocalizationButton />
54
+ </LocalizedProductProvider>
55
+ )
56
+
57
+ expect(mockUseBindingSelector).toHaveBeenCalledWith(
58
+ OTHER_LOCALES,
59
+ '/p',
60
+ 'roshe-tenis-76'
61
+ )
62
+ })
63
+
64
+ it('passes no default-locale slug on collection pages', () => {
65
+ render(
66
+ <LocalizedProductProvider otherLocales={OTHER_LOCALES} urlSuffix="">
67
+ <LocalizationButton />
68
+ </LocalizedProductProvider>
69
+ )
70
+
71
+ expect(mockUseBindingSelector).toHaveBeenCalledWith(
72
+ OTHER_LOCALES,
73
+ '',
74
+ undefined
75
+ )
76
+ })
77
+
78
+ it('falls back to product-page defaults outside a provider', () => {
79
+ render(<LocalizationButton />)
80
+
81
+ expect(mockUseBindingSelector).toHaveBeenCalledWith(
82
+ undefined,
83
+ '/p',
84
+ undefined
85
+ )
86
+ })
87
+ })
@@ -0,0 +1,252 @@
1
+ import { act, renderHook } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ vi.mock(import('../../../discovery.config.js'), async (original) => ({
5
+ default: {
6
+ ...((await original()).default ?? (await original())),
7
+ localization: {
8
+ enabled: true,
9
+ defaultLocale: 'en-US',
10
+ regions: {
11
+ US: {
12
+ code: 'US',
13
+ name: 'United States',
14
+ dateFormat: 'MM/DD/YYYY',
15
+ timeFormat: '12h',
16
+ timeFormatMask: 'hh:mm a',
17
+ unitSystem: 'imperial',
18
+ defaultTimezone: 'GMT-5',
19
+ },
20
+ BR: {
21
+ code: 'BR',
22
+ name: 'Brazil',
23
+ dateFormat: 'DD/MM/YYYY',
24
+ timeFormat: '24h',
25
+ timeFormatMask: 'HH:mm',
26
+ unitSystem: 'metric',
27
+ defaultTimezone: 'GMT-3',
28
+ },
29
+ IT: {
30
+ code: 'IT',
31
+ name: 'Italy',
32
+ dateFormat: 'DD/MM/YYYY',
33
+ timeFormat: '24h',
34
+ timeFormatMask: 'HH:mm',
35
+ unitSystem: 'metric',
36
+ defaultTimezone: 'GMT+1',
37
+ },
38
+ },
39
+ locales: {
40
+ 'en-US': {
41
+ code: 'en-US',
42
+ name: 'English',
43
+ languageCode: 'en',
44
+ languageName: 'English',
45
+ script: 'Latn',
46
+ textDirection: 'ltr',
47
+ regionCode: 'US',
48
+ bindings: [
49
+ {
50
+ currencyCode: 'USD',
51
+ url: 'https://store.example.com',
52
+ salesChannel: '1',
53
+ isDefault: true,
54
+ },
55
+ ],
56
+ },
57
+ 'pt-BR': {
58
+ code: 'pt-BR',
59
+ name: 'português',
60
+ languageCode: 'pt',
61
+ languageName: 'Portuguese',
62
+ script: 'Latn',
63
+ textDirection: 'ltr',
64
+ regionCode: 'BR',
65
+ bindings: [
66
+ {
67
+ currencyCode: 'BRL',
68
+ url: 'https://store.example.com/pt-BR',
69
+ salesChannel: '2',
70
+ isDefault: true,
71
+ },
72
+ ],
73
+ },
74
+ 'it-IT': {
75
+ code: 'it-IT',
76
+ name: 'italiano',
77
+ languageCode: 'it',
78
+ languageName: 'Italian',
79
+ script: 'Latn',
80
+ textDirection: 'ltr',
81
+ regionCode: 'IT',
82
+ bindings: [
83
+ {
84
+ currencyCode: 'EUR',
85
+ url: 'https://store.example.com/it-IT',
86
+ salesChannel: '3',
87
+ isDefault: true,
88
+ },
89
+ ],
90
+ },
91
+ },
92
+ currencies: {
93
+ USD: { code: 'USD', name: 'US Dollar', symbol: '$' },
94
+ BRL: { code: 'BRL', name: 'Brazilian Real', symbol: 'R$' },
95
+ EUR: { code: 'EUR', name: 'Euro', symbol: '€' },
96
+ },
97
+ },
98
+ },
99
+ }))
100
+
101
+ vi.mock('../../../src/sdk/session', () => ({
102
+ useSession: () => ({
103
+ locale: 'en-US',
104
+ currency: { code: 'USD', symbol: '$' },
105
+ }),
106
+ }))
107
+
108
+ import { useBindingSelector } from '../../../src/sdk/localization/useBindingSelector'
109
+
110
+ const TRANSLATED = [
111
+ { locale: 'en-US', slug: 'roshe-tenis-76' },
112
+ { locale: 'pt-BR', slug: 'tenis-roshe-76' },
113
+ ]
114
+
115
+ /**
116
+ * Replaces `globalThis.location` with a plain object so the redirect can be
117
+ * asserted. jsdom throws on a real `href` assignment.
118
+ */
119
+ function stubLocation(pathname: string, search = '', hash = '') {
120
+ const location = { pathname, search, hash, href: '' }
121
+ vi.stubGlobal('location', location)
122
+
123
+ return location
124
+ }
125
+
126
+ /** Drives the selector to a locale/currency pair and triggers the redirect. */
127
+ function switchTo(
128
+ locale: string,
129
+ currency: string,
130
+ otherLocales?: Array<{ locale: string; slug: string }> | null,
131
+ defaultLocaleSlug?: string | null,
132
+ urlSuffix = '/p'
133
+ ) {
134
+ const { result } = renderHook(() =>
135
+ useBindingSelector(otherLocales, urlSuffix, defaultLocaleSlug)
136
+ )
137
+
138
+ act(() => result.current.setLocaleCode(locale))
139
+ act(() => result.current.setCurrencyCode(currency))
140
+ act(() => result.current.save())
141
+
142
+ return result
143
+ }
144
+
145
+ describe('useBindingSelector redirect', () => {
146
+ beforeEach(() => {
147
+ globalThis.sessionStorage.clear()
148
+ })
149
+
150
+ afterEach(() => {
151
+ vi.unstubAllGlobals()
152
+ })
153
+
154
+ it('navigates to the slug the catalog registered for the target locale', () => {
155
+ const location = stubLocation('/roshe-tenis-76/p')
156
+
157
+ switchTo('pt-BR', 'BRL', TRANSLATED)
158
+
159
+ expect(location.href).toBe(
160
+ 'https://store.example.com/pt-BR/tenis-roshe-76/p'
161
+ )
162
+ })
163
+
164
+ it('falls back to the default locale slug registered in the map when the target has none', () => {
165
+ const location = stubLocation('/tenis-roshe-76/p')
166
+
167
+ switchTo('it-IT', 'EUR', TRANSLATED)
168
+
169
+ expect(location.href).toBe(
170
+ 'https://store.example.com/it-IT/roshe-tenis-76/p'
171
+ )
172
+ })
173
+
174
+ it('navigates to the product page via defaultLocaleSlug when the target locale has no registered slug', () => {
175
+ const location = stubLocation('/roshe-tenis-76/p')
176
+
177
+ switchTo(
178
+ 'it-IT',
179
+ 'EUR',
180
+ [{ locale: 'pt-BR', slug: 'tenis-roshe-76' }],
181
+ 'roshe-tenis-76'
182
+ )
183
+
184
+ // The Italian binding with the default-locale slug: an untranslated product
185
+ // page, which the agreed behavior prefers over the locale root.
186
+ expect(location.href).toBe(
187
+ 'https://store.example.com/it-IT/roshe-tenis-76/p'
188
+ )
189
+ })
190
+
191
+ it('uses defaultLocaleSlug for a product with no registered translations at all', () => {
192
+ const location = stubLocation('/side-by-side-refrigerator-14/p')
193
+
194
+ switchTo('pt-BR', 'BRL', [], 'side-by-side-refrigerator-14')
195
+
196
+ expect(location.href).toBe(
197
+ 'https://store.example.com/pt-BR/side-by-side-refrigerator-14/p'
198
+ )
199
+ })
200
+
201
+ it('never carries a slug registered for a third locale into the target locale', () => {
202
+ const location = stubLocation('/roshe-tenis-76/p')
203
+
204
+ switchTo(
205
+ 'it-IT',
206
+ 'EUR',
207
+ [{ locale: 'pt-BR', slug: 'tenis-roshe-76' }],
208
+ 'roshe-tenis-76'
209
+ )
210
+
211
+ expect(location.href).not.toContain('tenis-roshe-76')
212
+ })
213
+
214
+ it('falls back to the binding root on a PDP with no slug available', () => {
215
+ const location = stubLocation('/roshe-tenis-76/p')
216
+
217
+ switchTo('pt-BR', 'BRL', [], null)
218
+
219
+ expect(location.href).toBe('https://store.example.com/pt-BR')
220
+ })
221
+
222
+ it('preserves the query string and hash across the switch', () => {
223
+ const location = stubLocation('/roshe-tenis-76/p', '?skuId=76', '#reviews')
224
+
225
+ switchTo('pt-BR', 'BRL', TRANSLATED)
226
+
227
+ expect(location.href).toBe(
228
+ 'https://store.example.com/pt-BR/tenis-roshe-76/p?skuId=76#reviews'
229
+ )
230
+ })
231
+
232
+ it('omits the /p suffix for collection pages', () => {
233
+ const location = stubLocation('/apparel')
234
+
235
+ switchTo('pt-BR', 'BRL', [{ locale: 'pt-BR', slug: 'vestuario' }], null, '')
236
+
237
+ expect(location.href).toBe('https://store.example.com/pt-BR/vestuario')
238
+ })
239
+
240
+ it('reports an error instead of redirecting when the locale has no binding for the currency', () => {
241
+ const location = stubLocation('/roshe-tenis-76/p')
242
+
243
+ const result = switchTo('pt-BR', 'JPY', TRANSLATED)
244
+
245
+ expect(result.current.error).toEqual({
246
+ type: 'no-binding-found',
247
+ locale: 'pt-BR',
248
+ currency: 'JPY',
249
+ })
250
+ expect(location.href).toBe('')
251
+ })
252
+ })