@faststore/core 4.7.0-dev.8 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.7 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.0-dev.9 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.7 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.7.0-dev.9 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.7 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.7.0-dev.9 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.7 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.0-dev.9 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
@@ -7,19 +7,18 @@
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) 764ms
10
11
  Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
11
12
  npx update-browserslist-db@latest
12
13
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
13
- ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1025ms
14
- ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 2904ms
15
- ✓ shows the tab bar and renders Personal cards by default  1398ms
16
- ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 3445ms
17
- ✓ shows an error state when the list fails to load  1019ms
18
- ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  527ms
19
- ✓ keeps Confirm disabled until a different contract is selected  375ms
20
- ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 128ms
21
- ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 27ms
22
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 226ms
14
+ ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 1570ms
15
+ ✓ shows the tab bar and renders Personal cards by default  794ms
16
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2032ms
17
+ ✓ shows an error state when the list fails to load  520ms
18
+ ✓  browser  test/sdk/localization/useBindingSelector.browser.test.tsx (9 tests) 126ms
19
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 16ms
20
+ ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 172ms
21
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 107ms
23
22
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
24
23
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
25
24
   at node:internal/deps/undici/undici:16416:13
@@ -49,7 +48,9 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
49
48
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
50
49
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
51
50
 
52
- ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 203ms
51
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 118ms
52
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 654ms
53
+ ✓ renders the menu view with contract details and manage footer  420ms
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) 25ms
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
 
@@ -280,11 +280,9 @@ act(() => {
280
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
281
281
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
282
282
 
283
- ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1327ms
284
- ✓ renders the password protection form  546ms
285
- ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1086ms
286
- ✓ renders the menu view with contract details and manage footer  715ms
287
- ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 19ms
283
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 774ms
284
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 24ms
285
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 13ms
288
286
  stderr | test/sdk/localization/store-url.browser.test.ts
289
287
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
290
288
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -293,6 +291,7 @@ This ensures that you're testing the behavior the user would see in the browser.
293
291
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
294
292
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
295
293
 
294
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 4ms
296
295
  stderr | test/components/cms/RenderSections.browser.test.tsx
297
296
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
298
297
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -301,12 +300,11 @@ This ensures that you're testing the behavior the user would see in the browser.
301
300
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
302
301
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
303
302
 
304
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 6ms
305
- ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 43ms
306
- ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 61ms
307
- ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 39ms
308
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 134ms
309
- ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 60ms
303
+ ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 30ms
304
+ ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 64ms
305
+ ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 28ms
306
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 124ms
307
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 33ms
310
308
  stderr | test/components/account/doLogout.browser.test.ts
311
309
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
312
310
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -315,10 +313,11 @@ This ensures that you're testing the behavior the user would see in the browser.
315
313
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
316
314
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
317
315
 
318
- ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 33ms
319
- ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 85ms
320
- ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 293ms
321
- ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 277ms
316
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 22ms
317
+ ✓  browser  test/components/ui/LocalizationButton.browser.test.tsx (3 tests) 36ms
318
+ ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 71ms
319
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 267ms
320
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 191ms
322
321
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
323
322
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
324
323
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -328,11 +327,11 @@ This ensures that you're testing the behavior the user would see in the browser.
328
327
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
329
328
 
330
329
  ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 6ms
331
- ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 5ms
332
- ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 3ms
333
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 31ms
334
- ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 40ms
335
- ✓  node  test/server/password-protection-service.test.ts (22 tests) 31ms
330
+ ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 4ms
331
+ ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 2ms
332
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 23ms
333
+ ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 28ms
334
+ ✓  node  test/server/password-protection-service.test.ts (22 tests) 27ms
336
335
  stderr | test/sdk/account/refreshToken.browser.test.ts
337
336
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
338
337
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -341,11 +340,11 @@ This ensures that you're testing the behavior the user would see in the browser.
341
340
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
342
341
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
343
342
 
344
- ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 7ms
345
- ✓  node  test/utils/match-url.test.ts (10 tests) 11ms
346
- ✓  node  test/pages/api/unlock.test.ts (15 tests) 258ms
347
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 16ms
348
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 7ms
343
+ ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 4ms
344
+ ✓  node  test/utils/match-url.test.ts (10 tests) 7ms
345
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 12ms
346
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 125ms
347
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 12ms
349
348
  stderr | test/components/search/SearchInputComponent.test.tsx
350
349
  Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
351
350
 
@@ -409,19 +408,19 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
409
408
  at Suspense
410
409
  at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
411
410
 
412
- ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 76ms
413
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 46ms
414
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 12ms
411
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 65ms
412
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 37ms
413
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 11ms
415
414
  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
415
  { extensions: { status: 401 } }
417
416
 
418
417
  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
418
  { extensions: { status: 502 } }
420
419
 
421
- ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 11ms
422
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 26ms
423
- ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 9ms
424
- ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 444ms
420
+ ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 12ms
421
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 18ms
422
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 7ms
423
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 231ms
425
424
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
426
425
  Graphql execution returned with error: [
427
426
  {
@@ -470,7 +469,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
470
469
 
471
470
  stderr | test/pages/api/graphql.test.ts > /api/graphql request handling > maps BadRequestError thrown by execute to its status
472
471
  Something unexpected occurred querying Graphql endpoint:
473
- E [FastStoreError]: malformed
472
+ T [FastStoreError]: malformed
474
473
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:190:31
475
474
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:196:39
476
475
  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,66 +513,66 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
514
513
  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
514
  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
515
 
517
- ✓  node  test/pages/api/graphql.test.ts (17 tests) 20ms
518
- ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 836ms
519
- ✓ navigates with the search term on blur  364ms
520
- ✓ navigates without a label param when the search is cleared  361ms
516
+ ✓  node  test/pages/api/graphql.test.ts (17 tests) 16ms
521
517
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
522
518
  Set password request failed: Bad Request
523
519
 
524
- ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 50ms
525
- ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 34ms
526
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 8ms
527
- ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 40ms
528
- ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 7ms
529
- ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 12ms
530
- ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 53ms
531
- ✓  node  test/sdk/redirects/index.test.ts (7 tests) 54ms
532
- ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 150ms
533
- ✓  node  test/utils/productJsonLd.test.ts (17 tests) 10ms
534
- ✓  node  test/proxy.localization.test.ts (5 tests) 39ms
535
- ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 38ms
536
- ✓  node  test/server/index.test.ts (7 tests) 813ms
537
- ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 9ms
538
- ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 107ms
539
- ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 49ms
540
- ✓  node  test/proxy.test.ts (4 tests) 20ms
541
- ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 38ms
542
- ✓  node  test/server/webops-api.test.ts (9 tests) 12ms
543
- ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 6ms
544
- ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 11ms
545
- ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 14ms
546
- ✓  node  test/server/content/service.test.ts (5 tests) 6ms
547
- ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 11ms
548
- ✓  node  test/server/cms/global.test.ts (3 tests) 5ms
549
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 4ms
550
- ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 27ms
551
- ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 151ms
552
- ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 7ms
553
- ✓  node  test/pages/api/preview.test.ts (2 tests) 7ms
554
- ✓  node  test/server/rootSpan.test.ts (4 tests) 40ms
555
- ✓  node  test/components/account/Layout.test.tsx (6 tests) 66ms
556
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 6ms
557
- ✓  node  test/components/search/SearchInput.test.ts (6 tests) 9ms
520
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 19ms
521
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 15ms
522
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 3ms
523
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 21ms
524
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 792ms
525
+ ✓ navigates with the search term on blur  361ms
526
+ ✓ navigates without a label param when the search is cleared  357ms
527
+ ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 5ms
528
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 4ms
529
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 38ms
530
+ ✓  node  test/sdk/redirects/index.test.ts (7 tests) 37ms
531
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 102ms
532
+ ✓  node  test/utils/productJsonLd.test.ts (17 tests) 8ms
533
+ ✓  node  test/proxy.localization.test.ts (5 tests) 41ms
534
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 24ms
535
+ ✓  node  test/server/index.test.ts (7 tests) 699ms
536
+ ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 5ms
537
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 77ms
538
+ ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 31ms
539
+ ✓  node  test/server/webops-api.test.ts (9 tests) 11ms
540
+ ✓  node  test/proxy.test.ts (4 tests) 15ms
541
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 30ms
542
+ ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 9ms
543
+ ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 4ms
544
+ ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 9ms
545
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 5ms
546
+ ✓  node  test/server/content/service.test.ts (5 tests) 4ms
547
+ ✓  node  test/server/cms/global.test.ts (3 tests) 4ms
548
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 3ms
549
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 18ms
550
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 100ms
551
+ ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 3ms
552
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 6ms
553
+ ✓  node  test/server/rootSpan.test.ts (4 tests) 36ms
554
+ ✓  node  test/components/search/SearchInput.test.ts (6 tests) 6ms
555
+ ✓  node  test/components/account/Layout.test.tsx (6 tests) 60ms
556
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 4ms
558
557
  ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 4ms
559
- ✓  node  test/utils/retry.test.ts (5 tests) 15ms
558
+ ✓  node  test/utils/retry.test.ts (5 tests) 10ms
560
559
  ✓  node  test/server/cms/index.test.ts (2 tests) 6ms
561
560
  ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 tests) 4ms
562
- ✓  node  test/utils/unlockResponse.test.ts (11 tests) 5ms
563
- ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 6ms
564
- ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 193ms
565
- ✓  node  test/utils/isLocalHost.test.ts (7 tests) 4ms
566
- ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 47ms
567
- ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 4ms
561
+ ✓  node  test/utils/unlockResponse.test.ts (11 tests) 4ms
562
+ ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 3ms
563
+ ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 182ms
564
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 39ms
565
+ ✓  node  test/utils/isLocalHost.test.ts (7 tests) 3ms
566
+ ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 2ms
568
567
  ✓  node  test/utils/getCookie.test.ts (4 tests) 4ms
568
+ ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 29ms
569
569
  ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 3ms
570
- ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 36ms
571
- ✓  node  test/instrumentation.test.ts (3 tests) 6ms
572
- ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 12ms
573
- ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 25ms
574
-
575
-  Test Files  91 passed (91)
576
-  Tests  730 passed (730)
577
-  Start at  19:29:06
578
-  Duration  45.26s (transform 14.82s, setup 0ms, import 41.48s, tests 15.63s, environment 54.18s)
570
+ ✓  node  test/instrumentation.test.ts (3 tests) 4ms
571
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 22ms
572
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 19ms
573
+
574
+  Test Files  93 passed (93)
575
+  Tests  742 passed (742)
576
+  Start at  21:20:36
577
+  Duration  29.99s (transform 8.81s, setup 0ms, import 26.73s, tests 10.47s, environment 36.18s)
579
578
 
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](https://github.com/vtex/faststore/compare/v4.7.0-dev.9...v4.7.0) (2026-08-31)
7
+
8
+ **Note:** Version bump only for package @faststore/core
9
+
10
+ # [4.7.0-dev.9](https://github.com/vtex/faststore/compare/v4.7.0-dev.8...v4.7.0-dev.9) (2026-08-31)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **api:** separate hreflang slugs from locale-switch navigation ([#3462](https://github.com/vtex/faststore/issues/3462)) ([3091a46](https://github.com/vtex/faststore/commit/3091a46031f387d1e11e5747dd0f4ac6bef358e2))
15
+
6
16
  # [4.7.0-dev.8](https://github.com/vtex/faststore/compare/v4.7.0-dev.7...v4.7.0-dev.8) (2026-08-28)
7
17
 
8
18
  **Note:** Version bump only for package @faststore/core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.7.0-dev.8",
3
+ "version": "4.7.0",
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.8",
75
- "@faststore/lighthouse": "4.7.0-dev.8",
76
- "@faststore/diagnostics": "4.7.0-dev.8",
77
- "@faststore/ui": "4.7.0-dev.8",
78
- "@faststore/sdk": "4.7.0-dev.8"
74
+ "@faststore/diagnostics": "4.7.0",
75
+ "@faststore/lighthouse": "4.7.0",
76
+ "@faststore/sdk": "4.7.0",
77
+ "@faststore/ui": "4.7.0",
78
+ "@faststore/api": "4.7.0"
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
+ })