@faststore/core 4.7.0 → 4.7.1-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.9 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.1-dev.0 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.9 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.7.1-dev.0 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.9 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.7.1-dev.0 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.9 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.7.1-dev.0 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
@@ -7,18 +7,23 @@
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
11
- Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
10
+ ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1117ms
11
+ Browserslist: browsers data (caniuse-lite) is 7 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) 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
14
+ ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 2394ms
15
+ ✓ shows the tab bar and renders Personal cards by default  880ms
16
+ ✓ paginates when there are more than 25 cards  451ms
17
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 3084ms
18
+ ✓ shows an error state when the list fails to load  657ms
19
+ ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  469ms
20
+ ✓ keeps Confirm disabled until a different contract is selected  394ms
21
+ ✓ keeps Confirm disabled when switching is unavailable  319ms
22
+ ✓ keeps Confirm disabled when the selected contract is filtered out  390ms
23
+ ✓  browser  test/sdk/localization/useBindingSelector.browser.test.tsx (9 tests) 228ms
24
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 22ms
25
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 206ms
26
+ ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 196ms
22
27
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
23
28
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
24
29
   at node:internal/deps/undici/undici:16416:13
@@ -48,9 +53,7 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
48
53
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
49
54
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
50
55
 
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
56
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 241ms
54
57
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > posts the password and redirects to the requested return path
55
58
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
56
59
 
@@ -184,6 +187,8 @@ act(() => {
184
187
  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
188
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
186
189
 
190
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1055ms
191
+ ✓ renders the menu view with contract details and manage footer  706ms
187
192
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > shows service unavailable when the response is not a login payload
188
193
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
189
194
 
@@ -280,10 +285,11 @@ act(() => {
280
285
  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
286
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
282
287
 
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
286
- stderr | test/sdk/localization/store-url.browser.test.ts
288
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 928ms
289
+ ✓ renders the password protection form  470ms
290
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 9ms
291
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 19ms
292
+ stderr | test/components/cms/RenderSections.browser.test.tsx
287
293
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
288
294
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
289
295
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:120:9)
@@ -291,8 +297,7 @@ This ensures that you're testing the behavior the user would see in the browser.
291
297
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
292
298
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
293
299
 
294
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 4ms
295
- stderr | test/components/cms/RenderSections.browser.test.tsx
300
+ stderr | test/sdk/localization/store-url.browser.test.ts
296
301
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
297
302
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
298
303
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:120:9)
@@ -300,11 +305,12 @@ This ensures that you're testing the behavior the user would see in the browser.
300
305
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
301
306
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
302
307
 
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
308
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 5ms
309
+ ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 34ms
310
+ ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 97ms
311
+ ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 36ms
312
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 134ms
313
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 46ms
308
314
  stderr | test/components/account/doLogout.browser.test.ts
309
315
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
310
316
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -313,11 +319,10 @@ This ensures that you're testing the behavior the user would see in the browser.
313
319
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
314
320
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
315
321
 
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
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 19ms
323
+ ✓  browser  test/components/ui/LocalizationButton.browser.test.tsx (3 tests) 49ms
324
+ ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 91ms
325
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 288ms
321
326
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
322
327
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
323
328
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -326,12 +331,12 @@ This ensures that you're testing the behavior the user would see in the browser.
326
331
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
327
332
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
328
333
 
334
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 307ms
329
335
  ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 6ms
330
336
  ✓  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
337
+ ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 4ms
338
+ ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 37ms
339
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 29ms
335
340
  stderr | test/sdk/account/refreshToken.browser.test.ts
336
341
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
337
342
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -341,10 +346,11 @@ This ensures that you're testing the behavior the user would see in the browser.
341
346
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
342
347
 
343
348
  ✓  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
+ ✓  node  test/server/password-protection-service.test.ts (22 tests) 35ms
350
+ ✓  node  test/utils/match-url.test.ts (10 tests) 10ms
351
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 15ms
352
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 191ms
353
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 10ms
348
354
  stderr | test/components/search/SearchInputComponent.test.tsx
349
355
  Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
350
356
 
@@ -408,19 +414,19 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
408
414
  at Suspense
409
415
  at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
410
416
 
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
417
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 85ms
418
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 51ms
419
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 17ms
414
420
  stderr | test/pages/pvt/account/cards/index.test.ts > Cards page getServerSideProps > redirects to /pvt/account/403 when the query fails with 401/403
415
421
  { extensions: { status: 401 } }
416
422
 
417
423
  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
418
424
  { extensions: { status: 502 } }
419
425
 
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
426
+ ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 9ms
427
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 27ms
428
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 12ms
429
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 370ms
424
430
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
425
431
  Graphql execution returned with error: [
426
432
  {
@@ -469,7 +475,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
469
475
 
470
476
  stderr | test/pages/api/graphql.test.ts > /api/graphql request handling > maps BadRequestError thrown by execute to its status
471
477
  Something unexpected occurred querying Graphql endpoint:
472
- T [FastStoreError]: malformed
478
+ E [FastStoreError]: malformed
473
479
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:190:31
474
480
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:196:39
475
481
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1024:60
@@ -485,7 +491,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
485
491
 
486
492
  stderr | test/pages/api/graphql.test.ts > /api/graphql request handling > maps UnauthorizedError thrown by execute to its status
487
493
  Something unexpected occurred querying Graphql endpoint:
488
- re [FastStoreError]: expired
494
+ ie [FastStoreError]: expired
489
495
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:191:33
490
496
  at /home/runner/work/faststore/faststore/packages/core/test/pages/api/graphql.test.ts:196:39
491
497
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1024:60
@@ -513,66 +519,68 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
513
519
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37
514
520
  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)
515
521
 
516
- ✓  node  test/pages/api/graphql.test.ts (17 tests) 16ms
522
+ ✓  node  test/pages/api/graphql.test.ts (17 tests) 36ms
517
523
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
518
524
  Set password request failed: Bad Request
519
525
 
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
557
- ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 4ms
558
- ✓  node  test/utils/retry.test.ts (5 tests) 10ms
559
- ✓  node  test/server/cms/index.test.ts (2 tests) 6ms
560
- ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 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
567
- ✓  node  test/utils/getCookie.test.ts (4 tests) 4ms
568
- ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 29ms
569
- ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 3ms
526
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 34ms
527
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 835ms
528
+ ✓ navigates with the search term on blur  363ms
529
+ ✓ navigates without a label param when the search is cleared  359ms
530
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 45ms
531
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 37ms
532
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 7ms
533
+ ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 8ms
534
+ ✓  node  test/utils/localization/hreflang.test.ts (8 tests) 8ms
535
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 10ms
536
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 50ms
537
+ ✓  node  test/sdk/redirects/index.test.ts (7 tests) 45ms
538
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 163ms
539
+ ✓  node  test/utils/productJsonLd.test.ts (17 tests) 10ms
540
+ ✓  node  test/proxy.localization.test.ts (5 tests) 29ms
541
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 39ms
542
+ ✓  node  test/server/index.test.ts (7 tests) 953ms
543
+ ✓ should handle options and execute  453ms
544
+ ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 9ms
545
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 127ms
546
+ ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 48ms
547
+ ✓  node  test/server/webops-api.test.ts (9 tests) 18ms
548
+ ✓  node  test/proxy.test.ts (4 tests) 20ms
549
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 45ms
550
+ ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 12ms
551
+ ✓  node  test/sdk/localization/store-url.test.ts (4 tests) 5ms
552
+ ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 7ms
553
+ ✓  node  test/server/content/service.test.ts (5 tests) 5ms
554
+ ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 10ms
555
+ ✓  node  test/server/cms/global.test.ts (3 tests) 5ms
556
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 8ms
557
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 36ms
558
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 167ms
559
+ ✓  node  test/server/rootSpan.test.ts (4 tests) 46ms
560
+ ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 5ms
561
+ ✓  node  test/components/search/SearchInput.test.ts (6 tests) 9ms
562
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 10ms
563
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 5ms
564
+ ✓  node  test/components/account/Layout.test.tsx (6 tests) 54ms
565
+ ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 5ms
566
+ ✓  node  test/server/cms/index.test.ts (2 tests) 4ms
567
+ ✓  node  test/utils/retry.test.ts (5 tests) 14ms
568
+ ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 tests) 6ms
569
+ ✓  node  test/utils/unlockResponse.test.ts (11 tests) 6ms
570
+ ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 4ms
571
+ ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 197ms
572
+ ✓  node  test/utils/isLocalHost.test.ts (7 tests) 5ms
573
+ ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 4ms
574
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 54ms
575
+ ✓  node  test/utils/getCookie.test.ts (4 tests) 5ms
576
+ ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 4ms
570
577
  ✓  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)
578
+ ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 42ms
579
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 14ms
580
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 25ms
581
+
582
+  Test Files  94 passed (94)
583
+  Tests  753 passed (753)
584
+  Start at  23:40:57
585
+  Duration  45.98s (transform 12.14s, setup 0ms, import 40.59s, tests 14.88s, environment 57.52s)
578
586
 
package/@generated/gql.ts CHANGED
@@ -29,7 +29,7 @@ type Documents = {
29
29
  "\n fragment ServerCollectionPage on Query {\n collection(slug: $slug) {\n id\n }\n }\n": typeof types.ServerCollectionPageFragmentDoc,
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
- "\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,
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 canonicalSlug\n }\n }\n": typeof types.ServerCollectionPageQueryDocument,
33
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,
@@ -82,7 +82,7 @@ const documents: Documents = {
82
82
  "\n fragment ServerCollectionPage on Query {\n collection(slug: $slug) {\n id\n }\n }\n": types.ServerCollectionPageFragmentDoc,
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
- "\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,
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 canonicalSlug\n }\n }\n": types.ServerCollectionPageQueryDocument,
86
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,
@@ -180,7 +180,7 @@ export function gql(source: "\n query ServerAccountPageQuery {\n accountProf
180
180
  /**
181
181
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
182
182
  */
183
- export function gql(source: "\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 import('./graphql').ServerCollectionPageQueryDocument;
183
+ export function gql(source: "\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 canonicalSlug\n }\n }\n"): typeof import('./graphql').ServerCollectionPageQueryDocument;
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
  */
@@ -1345,13 +1345,24 @@ export type StoreCartMessage = {
1345
1345
  export type StoreCollection = {
1346
1346
  /** List of items consisting of chain linked web pages, ending with the current page. */
1347
1347
  breadcrumbList: StoreBreadcrumbList;
1348
+ /**
1349
+ * Slug the catalog resolves for the requested locale, lowercased.
1350
+ * Meant for the page's canonical URL: `slug` echoes the path the shopper
1351
+ * visited, and by-linkid matches case-insensitively, so without this every
1352
+ * casing variant would claim to be canonical. Unlike `otherLocales`, it may fall
1353
+ * back to a slug registered for another locale, so it must never be used to
1354
+ * build hreflang annotations.
1355
+ */
1356
+ canonicalSlug: Maybe<Scalars['String']['output']>;
1348
1357
  /** Collection ID. */
1349
1358
  id: Scalars['ID']['output'];
1350
1359
  /** Collection meta information. Used for search. */
1351
1360
  meta: StoreCollectionMeta;
1352
1361
  /**
1353
1362
  * Localized versions of this collection for all available locales.
1354
- * Only populated when localization is enabled.
1363
+ * Only announces slugs the catalog has registered for each locale, so it is safe
1364
+ * to build hreflang annotations from. Only populated when localization is
1365
+ * enabled.
1355
1366
  */
1356
1367
  otherLocales: Maybe<Array<StoreCollectionLocale>>;
1357
1368
  /** Meta tag data. */
@@ -2820,7 +2831,7 @@ export type ServerCollectionPageQueryQueryVariables = Exact<{
2820
2831
  }>;
2821
2832
 
2822
2833
 
2823
- export type ServerCollectionPageQueryQuery = { collection: { id: string, seo: { title: string, description: string }, breadcrumbList: { itemListElement: Array<{ item: string, name: string, position: number }> }, meta: { selectedFacets: Array<{ key: string, value: string }> }, otherLocales: Array<{ locale: string, slug: string }> | null } };
2834
+ export type ServerCollectionPageQueryQuery = { collection: { canonicalSlug: string | null, id: string, seo: { title: string, description: string }, breadcrumbList: { itemListElement: Array<{ item: string, name: string, position: number }> }, meta: { selectedFacets: Array<{ key: string, value: string }> }, otherLocales: Array<{ locale: string, slug: string }> | null } };
2824
2835
 
2825
2836
  export type ServerProductQueryQueryVariables = Exact<{
2826
2837
  locator: Array<IStoreSelectedFacet> | IStoreSelectedFacet;
@@ -3624,7 +3635,7 @@ export const SearchEvent_MetadataFragmentDoc = new TypedDocumentString(`
3624
3635
  }
3625
3636
  `, {"fragmentName":"SearchEvent_metadata"}) as unknown as TypedDocumentString<SearchEvent_MetadataFragment, unknown>;
3626
3637
  export const ServerAccountPageQueryDocument = {"__meta__":{"operationName":"ServerAccountPageQuery","operationHash":"9baae331b75848a310fecb457e8c971ae27897ff"}} as unknown as TypedDocumentString<ServerAccountPageQueryQuery, ServerAccountPageQueryQueryVariables>;
3627
- export const ServerCollectionPageQueryDocument = {"__meta__":{"operationName":"ServerCollectionPageQuery","operationHash":"d46841b30ae1f6350021b5cf02f253d56c848664"}} as unknown as TypedDocumentString<ServerCollectionPageQueryQuery, ServerCollectionPageQueryQueryVariables>;
3638
+ export const ServerCollectionPageQueryDocument = {"__meta__":{"operationName":"ServerCollectionPageQuery","operationHash":"fa5f52d2e0c1c7905c1d8572b81ce48d62265a14"}} as unknown as TypedDocumentString<ServerCollectionPageQueryQuery, ServerCollectionPageQueryQueryVariables>;
3628
3639
  export const ServerProductQueryDocument = {"__meta__":{"operationName":"ServerProductQuery","operationHash":"13fc9efa8628b91971e3a8aa985e6cdbed70adae"}} as unknown as TypedDocumentString<ServerProductQueryQuery, ServerProductQueryQueryVariables>;
3629
3640
  export const ServerListCardsQueryDocument = {"__meta__":{"operationName":"ServerListCardsQuery","operationHash":"392cf85d18d66b94d6ea8d6b2a3c6ffb5c94683d"}} as unknown as TypedDocumentString<ServerListCardsQueryQuery, ServerListCardsQueryQueryVariables>;
3630
3641
  export const ServerOrderDetailsQueryDocument = {"__meta__":{"operationName":"ServerOrderDetailsQuery","operationHash":"bdf677bbccce12186a5ef15aebdce46585a99782"}} as unknown as TypedDocumentString<ServerOrderDetailsQueryQuery, ServerOrderDetailsQueryQueryVariables>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "9baae331b75848a310fecb457e8c971ae27897ff": "query ServerAccountPageQuery { accountProfile { name } }",
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 }",
3
+ "fa5f52d2e0c1c7905c1d8572b81ce48d62265a14": "fragment ServerCollectionPage on Query { collection(slug: $slug) { id } } query ServerCollectionPageQuery($slug: String!) { collection(slug: $slug) { breadcrumbList { itemListElement { item name position } } canonicalSlug meta { selectedFacets { key value } } otherLocales { locale slug } seo { description title } } ...ServerCollectionPage }",
4
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 } } }",
@@ -1330,9 +1330,20 @@ type StoreCollection {
1330
1330
  type: StoreCollectionType!
1331
1331
  """
1332
1332
  Localized versions of this collection for all available locales.
1333
- Only populated when localization is enabled.
1333
+ Only announces slugs the catalog has registered for each locale, so it is safe
1334
+ to build hreflang annotations from. Only populated when localization is
1335
+ enabled.
1334
1336
  """
1335
1337
  otherLocales: [StoreCollectionLocale!]
1338
+ """
1339
+ Slug the catalog resolves for the requested locale, lowercased.
1340
+ Meant for the page's canonical URL: `slug` echoes the path the shopper
1341
+ visited, and by-linkid matches case-insensitively, so without this every
1342
+ casing variant would claim to be canonical. Unlike `otherLocales`, it may fall
1343
+ back to a slug registered for another locale, so it must never be used to
1344
+ build hreflang annotations.
1345
+ """
1346
+ canonicalSlug: String
1336
1347
  }
1337
1348
 
1338
1349
  """
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.1-dev.1](https://github.com/vtex/faststore/compare/v4.7.1-dev.0...v4.7.1-dev.1) (2026-09-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ - point localized canonical and hreflang at the rendered locale ([#3470](https://github.com/vtex/faststore/issues/3470)) ([d9038cf](https://github.com/vtex/faststore/commit/d9038cf631cf40eae5b9d9e3eb2c33290eba7b73))
11
+
12
+ ## [4.7.1-dev.0](https://github.com/vtex/faststore/compare/v4.7.0...v4.7.1-dev.0) (2026-08-31)
13
+
14
+ **Note:** Version bump only for package @faststore/core
15
+
6
16
  # [4.7.0](https://github.com/vtex/faststore/compare/v4.7.0-dev.9...v4.7.0) (2026-08-31)
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",
3
+ "version": "4.7.1-dev.1",
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/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"
74
+ "@faststore/api": "4.7.1-dev.1",
75
+ "@faststore/diagnostics": "4.7.1-dev.1",
76
+ "@faststore/ui": "4.7.1-dev.1",
77
+ "@faststore/sdk": "4.7.1-dev.1",
78
+ "@faststore/lighthouse": "4.7.1-dev.1"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@cypress/code-coverage": "^3.12.1",
@@ -1,4 +1,5 @@
1
1
  import { NextSeo, SiteLinksSearchBoxJsonLd } from 'next-seo'
2
+ import { useRouter } from 'next/router'
2
3
  import type { ComponentType } from 'react'
3
4
 
4
5
  import { default as GLOBAL_COMPONENTS } from 'src/components/cms/global/Components'
@@ -53,6 +54,7 @@ export default function LandingPage({
53
54
  globalSections,
54
55
  globalSettings,
55
56
  }: LandingPageProps) {
57
+ const router = useRouter()
56
58
  const context = {
57
59
  data: serverData,
58
60
  globalSettings,
@@ -63,7 +65,7 @@ export default function LandingPage({
63
65
  storeConfig.seo.titleTemplate ??
64
66
  storeConfig.seo.title
65
67
 
66
- const storeURL = getStoreURL()
68
+ const storeURL = getStoreURL(router.locale)
67
69
 
68
70
  return (
69
71
  <>
@@ -24,6 +24,7 @@ import { faststoreLoader } from 'src/components/ui/Image/loader'
24
24
  import { LocalizedProductProvider } from 'src/sdk/localization/LocalizedProductContext'
25
25
  import ProductListing from './ProductListing'
26
26
  import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'
27
+ import { buildHreflangLinks } from 'src/utils/localization/hreflang'
27
28
 
28
29
  export type ProductListingPageProps = {
29
30
  data: ServerCollectionPageQueryQuery & ServerManyProductsQueryQuery
@@ -105,10 +106,22 @@ export default function ProductListingPage({
105
106
  collection?.seo.description || // Use description that comes from the Checkout API
106
107
  plpSeo?.descriptionTemplate?.replace(/%s/g, () => title) || // Use description template from the SEO config for PLP
107
108
  storeSeo.description // Use default description from the store SEO config
108
- const storeURL = getStoreURL()
109
-
109
+ const otherLocales = server.collection?.otherLocales
110
+ const storeURL = getStoreURL(router.locale).replace(/\/$/, '')
111
+
112
+ // The slug the catalog resolves, not the one the shopper typed: by-linkid
113
+ // matches case-insensitively, so /Apparel and /apparel are the same page and
114
+ // must not each claim to be canonical. `otherLocales` is deliberately not
115
+ // used here — it only carries registered translations, so a page reached
116
+ // through a locale with no translation of its own would have no canonical at
117
+ // all. Falls back to the visited path when the backend resolves no slug.
118
+ const canonicalSlug = server.collection?.canonicalSlug
110
119
  const [pathname] = router.asPath.split('?')
111
- const canonical = `${storeURL}${pathname}`
120
+ const canonical = canonicalSlug
121
+ ? `${storeURL}/${canonicalSlug}`
122
+ : `${storeURL}${pathname}`
123
+
124
+ const hreflangLinks = buildHreflangLinks(otherLocales, router.locale)
112
125
  const itemsPerPage = settings?.productGallery?.itemsPerPage ?? ITEMS_PER_PAGE
113
126
 
114
127
  let itemListElements = collection?.breadcrumbList.itemListElement ?? []
@@ -143,10 +156,7 @@ export default function ProductListingPage({
143
156
  : undefined
144
157
 
145
158
  return (
146
- <LocalizedProductProvider
147
- otherLocales={server.collection?.otherLocales}
148
- urlSuffix=""
149
- >
159
+ <LocalizedProductProvider otherLocales={otherLocales} urlSuffix="">
150
160
  <SearchProvider
151
161
  onChange={applySearchState}
152
162
  itemsPerPage={itemsPerPage}
@@ -169,6 +179,7 @@ export default function ProductListingPage({
169
179
  description={description}
170
180
  titleTemplate={titleTemplate}
171
181
  canonical={canonical}
182
+ languageAlternates={hreflangLinks}
172
183
  openGraph={{
173
184
  type: 'website',
174
185
  title,
@@ -110,6 +110,7 @@ const query = gql(`
110
110
  locale
111
111
  slug
112
112
  }
113
+ canonicalSlug
113
114
  }
114
115
  }
115
116
  `)
@@ -3,6 +3,7 @@ import deepmerge from 'deepmerge'
3
3
  import type { GetStaticPaths, GetStaticProps } from 'next'
4
4
  import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo'
5
5
  import Head from 'next/head'
6
+ import { useRouter } from 'next/router'
6
7
  import type { ComponentType } from 'react'
7
8
 
8
9
  import { gql } from '@generated'
@@ -28,6 +29,7 @@ import { useSession } from 'src/sdk/session'
28
29
  import { execute } from 'src/server'
29
30
  import { getComponentKey } from 'src/utils/cms'
30
31
  import { getChannelForLocale } from 'src/utils/localization/bindingPaths'
32
+ import { buildHreflangLinks } from 'src/utils/localization/hreflang'
31
33
  import { toProductJsonLdOffer } from 'src/utils/productJsonLd'
32
34
 
33
35
  import storeConfig from 'discovery.config'
@@ -97,46 +99,6 @@ const overwriteMerge = (_: any[], sourceArray: any[]) => sourceArray
97
99
  const isClientOfferEnabled = (storeConfig as StoreConfig).experimental
98
100
  .enableClientOffer
99
101
 
100
- function buildHreflangLinks(
101
- storeConfig: StoreConfig,
102
- otherLocales: Array<{ locale: string; slug: string }> | null | undefined
103
- ): Array<{ rel: string; hrefLang: string; href: string }> {
104
- if (!storeConfig.localization?.enabled || !otherLocales?.length) return []
105
-
106
- const locales = storeConfig.localization.locales
107
- const baseStoreUrl = storeConfig.storeUrl.replace(/\/$/, '')
108
- const defaultLocale = storeConfig.localization.defaultLocale
109
- const links: Array<{ rel: string; hrefLang: string; href: string }> = []
110
-
111
- for (const { locale, slug } of otherLocales) {
112
- const bindingUrl = locales?.[locale]?.bindings?.[0]?.url
113
- if (typeof bindingUrl === 'string' && bindingUrl.length > 0) {
114
- links.push({
115
- rel: 'alternate',
116
- hrefLang: locale,
117
- href: `${bindingUrl.replace(/\/$/, '')}/${slug}/p`,
118
- })
119
- }
120
- }
121
-
122
- // PREMISE: the default locale is served at the store root (no locale prefix),
123
- // which is how Next.js i18n sub-path routing works — the `defaultLocale` has no
124
- // prefix while other locales live under `/{locale}`. Hence `x-default` points to
125
- // `${storeUrl}/{slug}/p`. If a store ever serves its default locale under a path
126
- // prefix or a dedicated domain instead of the root, this href must be derived
127
- // from that locale's binding URL instead of `storeUrl`.
128
- const defaultEntry = otherLocales.find((e) => e.locale === defaultLocale)
129
- if (defaultEntry) {
130
- links.push({
131
- rel: 'alternate',
132
- hrefLang: 'x-default',
133
- href: `${baseStoreUrl}/${defaultEntry.slug}/p`,
134
- })
135
- }
136
-
137
- return links
138
- }
139
-
140
102
  // With client-side offer enabled, `useOffer` only refreshes price aggregates —
141
103
  // it does not rebuild the per-seller array the buy button reads. Keep that array
142
104
  // from SSG, but inject the fresh Pricing Fallback token into the best offer so the
@@ -162,6 +124,7 @@ function Page({
162
124
  meta,
163
125
  }: Props) {
164
126
  const { currency } = useSession()
127
+ const router = useRouter()
165
128
 
166
129
  const { product } = server
167
130
  const {
@@ -197,15 +160,16 @@ function Page({
197
160
 
198
161
  // hreflang alternate links for multi-locale stores
199
162
  const hreflangLinks = buildHreflangLinks(
200
- storeConfig as StoreConfig,
201
- server.product.otherLocales
163
+ server.product.otherLocales,
164
+ router.locale,
165
+ '/p'
202
166
  )
203
167
 
204
168
  let itemListElements = product.breadcrumbList.itemListElement ?? []
205
169
  if (itemListElements.length !== 0) {
206
170
  itemListElements = itemListElements.map(
207
171
  ({ item: pathname, name, position }) => {
208
- const pageUrl = getStoreURL() + pathname
172
+ const pageUrl = getStoreURL(router.locale) + pathname
209
173
 
210
174
  return { name, position, item: pageUrl }
211
175
  }
@@ -314,7 +278,7 @@ function Page({
314
278
  ]
315
279
  : []),
316
280
  ]}
317
- additionalLinkTags={hreflangLinks}
281
+ languageAlternates={hreflangLinks}
318
282
  titleTemplate={titleTemplate}
319
283
  />
320
284
 
@@ -512,7 +476,10 @@ export const getStaticProps: GetStaticProps<
512
476
  const { seo } = data.product
513
477
  const title = seo.title
514
478
  const description = seo.description
515
- const canonical = `${getStoreURL()}${seo.canonical}`
479
+ // Without the locale, static rendering has no request to match a binding
480
+ // against and falls back to the default locale, so a localized PDP would
481
+ // point its canonical at the default locale's prefix.
482
+ const canonical = `${getStoreURL(locale).replace(/\/$/, '')}${seo.canonical}`
516
483
 
517
484
  const meta = { title, description, canonical }
518
485
 
@@ -1,5 +1,6 @@
1
1
  import type { GetStaticProps } from 'next'
2
2
  import { NextSeo, OrganizationJsonLd, SiteLinksSearchBoxJsonLd } from 'next-seo'
3
+ import { useRouter } from 'next/router'
3
4
 
4
5
  import RenderSections from 'src/components/cms/RenderSections'
5
6
  import type { PageContentType } from 'src/server/cms'
@@ -28,6 +29,7 @@ function Page({
28
29
  globalSections: globalSectionsProp,
29
30
  serverData,
30
31
  }: Props) {
32
+ const router = useRouter()
31
33
  const { sections: globalSections, settings: globalSettings } =
32
34
  globalSectionsProp ?? {}
33
35
  const context = {
@@ -53,7 +55,7 @@ function Page({
53
55
  {} as Record<string, string>
54
56
  )
55
57
 
56
- const storeUrl = getStoreURL()
58
+ const storeUrl = getStoreURL(router.locale)
57
59
 
58
60
  return (
59
61
  <>
@@ -28,6 +28,38 @@ export function getSettingsFromConfig(
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Resolves a locale's settings from configuration alone, without a request URL.
33
+ *
34
+ * Statically rendered pages have no request to match a binding against, so
35
+ * `getSettings` falls back to the default locale on the server and every
36
+ * server-rendered URL ends up describing the default locale rather than the page
37
+ * being built. Next resolves the locale for us during rendering, and this maps
38
+ * it back to a binding.
39
+ *
40
+ * The binding tie-break matches the rest of the localization layer (see
41
+ * `getChannelForLocale`): the one marked default, else the first declared.
42
+ * Returns null when the locale is unknown or has no usable binding, leaving the
43
+ * caller on its existing fallback.
44
+ */
45
+ export function getSettingsForLocale(
46
+ locale?: string
47
+ ): LocalizationSettings | null {
48
+ if (!locale || !config.localization?.enabled) return null
49
+
50
+ const localeConfig = config.localization.locales?.[locale]
51
+
52
+ if (!localeConfig) return null
53
+
54
+ const binding =
55
+ localeConfig.bindings?.find((el) => el.isDefault) ??
56
+ localeConfig.bindings?.at(0)
57
+
58
+ if (!binding) return null
59
+
60
+ return getSettingsFromConfig(localeConfig, binding)
61
+ }
62
+
31
63
  export function getSettings(params?: {
32
64
  url?: string | URL
33
65
  }): LocalizationSettings {
@@ -2,7 +2,11 @@ import config from 'discovery.config'
2
2
  import deepEqual from 'fast-deep-equal'
3
3
  import React, { useEffect, useRef, useState } from 'react'
4
4
  import { sessionStore } from '../session'
5
- import { type LocalizationSettings, getSettings } from './settings'
5
+ import {
6
+ type LocalizationSettings,
7
+ getSettings,
8
+ getSettingsForLocale,
9
+ } from './settings'
6
10
 
7
11
  export const useLocalizationConfig = (params?: { url?: string | URL }) => {
8
12
  const defaultConfig = config.localization.locales[
@@ -66,13 +70,22 @@ export const useLocalizationConfig = (params?: { url?: string | URL }) => {
66
70
  type ConfigType =
67
71
  (typeof config)['localization']['locales'][keyof (typeof config)['localization']['locales']]
68
72
 
69
- export const getStoreURL = () => {
73
+ /**
74
+ * Base URL of the storefront the current page belongs to.
75
+ *
76
+ * @param locale - Locale being rendered, as resolved by Next. Required for
77
+ * anything a crawler reads (canonical, hreflang, JSON-LD): without it, server
78
+ * rendering has no `window.location` to match a binding against and silently
79
+ * falls back to the default locale's binding, so a localized page would
80
+ * advertise URLs under the default locale's prefix.
81
+ */
82
+ export const getStoreURL = (locale?: string) => {
70
83
  // If localization is not enabled, use storeUrl from config
71
84
  if (!config.localization?.enabled === true) {
72
85
  return config.storeUrl
73
86
  }
74
87
 
75
- return getSettings().storeURL
88
+ return getSettingsForLocale(locale)?.storeURL ?? getSettings().storeURL
76
89
  }
77
90
 
78
91
  /** @description receives an react component and returns a new component with the storeURL as optional */
@@ -0,0 +1,73 @@
1
+ import storeConfig from 'discovery.config'
2
+ import { getSettingsForLocale } from 'src/sdk/localization/settings'
3
+ import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'
4
+
5
+ export type LocaleSlug = { locale: string; slug: string }
6
+
7
+ export type HreflangLink = { hrefLang: string; href: string }
8
+
9
+ /**
10
+ * Builds the hreflang cluster for a page the catalog resolves in more than one
11
+ * locale.
12
+ *
13
+ * `otherLocales` carries registered translations only, so a page reached through
14
+ * a locale that never registered a slug for it is missing from its own cluster.
15
+ * Search engines ignore a set that does not reference itself, which would make
16
+ * those annotations inert while still being reported as errors, so the cluster
17
+ * is dropped entirely rather than published without a self-reference.
18
+ *
19
+ * Feed the result to NextSeo's `languageAlternates`, not `additionalLinkTags`:
20
+ * the latter keys tags by href, so the default locale and `x-default` share a
21
+ * key and Next drops one of them.
22
+ *
23
+ * @param otherLocales - Slug registered for each locale that resolves this page
24
+ * @param currentLocale - Locale being rendered. Next leaves it undefined only
25
+ * when i18n is off, which means a single locale: the default one
26
+ * @param urlSuffix - Appended after the slug: '/p' for products, '' for collections
27
+ */
28
+ export function buildHreflangLinks(
29
+ otherLocales: LocaleSlug[] | null | undefined,
30
+ currentLocale: string | undefined,
31
+ urlSuffix = ''
32
+ ): HreflangLink[] {
33
+ if (!storeConfig.localization?.enabled || !otherLocales?.length) return []
34
+
35
+ const defaultLocale = storeConfig.localization.defaultLocale
36
+ const renderedLocale = currentLocale ?? defaultLocale
37
+
38
+ if (!otherLocales.some(({ locale }) => locale === renderedLocale)) return []
39
+
40
+ const links: HreflangLink[] = otherLocales
41
+ .filter(({ locale }) => {
42
+ const settings = getSettingsForLocale(locale)
43
+ if (!settings) {
44
+ console.warn(
45
+ `[hreflang] locale "${locale}" has no usable binding in discoveryConfig — skipped`
46
+ )
47
+ }
48
+ return settings !== null
49
+ })
50
+ .map(({ locale, slug }) => ({
51
+ hrefLang: locale,
52
+ href: `${getStoreURL(locale).replace(/\/$/, '')}/${slug}${urlSuffix}`,
53
+ }))
54
+
55
+ // x-default is what a shopper gets when no advertised locale matches, so it
56
+ // has to be a URL that is canonical in its own right. That is the default
57
+ // locale's binding, not the store root: a store whose default locale is served
58
+ // under a prefix or its own domain answers at the root too, but that root URL
59
+ // canonicalizes elsewhere and would send the cluster to a page that disclaims
60
+ // itself.
61
+ const defaultEntry = otherLocales.find(
62
+ ({ locale }) => locale === defaultLocale
63
+ )
64
+
65
+ if (defaultEntry) {
66
+ links.push({
67
+ hrefLang: 'x-default',
68
+ href: `${getStoreURL(defaultLocale).replace(/\/$/, '')}/${defaultEntry.slug}${urlSuffix}`,
69
+ })
70
+ }
71
+
72
+ return links
73
+ }
@@ -93,4 +93,20 @@ describe('localization store url', () => {
93
93
  config.localization.locales['pt-BR'].bindings[0].url
94
94
  )
95
95
  })
96
+
97
+ // Static rendering has no window.location to match a binding against, so
98
+ // without a locale every page describes itself with the default locale's URL.
99
+ it('returns the requested locale binding instead of the default one', () => {
100
+ expect(getStoreURL('en-US')).toEqual('https://brandless.fast.store/en-US')
101
+ })
102
+
103
+ it('prefers the binding marked default over declaration order', () => {
104
+ expect(getStoreURL('pt-BR')).toEqual('https://brandless.myvtex.com')
105
+ })
106
+
107
+ it('falls back to the default locale for a locale that is not configured', () => {
108
+ expect(getStoreURL('fr-FR')).toEqual(
109
+ config.localization.locales['pt-BR'].bindings[0].url
110
+ )
111
+ })
96
112
  })
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { buildHreflangLinks } from '../../../src/utils/localization/hreflang.js'
3
+
4
+ vi.mock(import('../../../discovery.config.js'), async (original) => ({
5
+ default: {
6
+ ...((await original()).default ?? {}),
7
+ localization: {
8
+ enabled: true,
9
+ defaultLocale: 'en-US',
10
+ regions: {},
11
+ locales: {
12
+ 'en-US': {
13
+ code: 'en-US',
14
+ bindings: [
15
+ {
16
+ currencyCode: 'USD',
17
+ // The default locale is served under a prefix, not at the root.
18
+ url: 'https://brandless.fast.store/en-US',
19
+ salesChannel: '1',
20
+ isDefault: true,
21
+ },
22
+ ],
23
+ },
24
+ 'pt-BR': {
25
+ code: 'pt-BR',
26
+ bindings: [
27
+ {
28
+ currencyCode: 'BRL',
29
+ url: 'https://brandless.fast.store/pt-BR',
30
+ salesChannel: '2',
31
+ isDefault: true,
32
+ },
33
+ ],
34
+ },
35
+ 'it-IT': {
36
+ code: 'it-IT',
37
+ bindings: [
38
+ {
39
+ currencyCode: 'USD',
40
+ // Bindings are arbitrary paths, not locale codes.
41
+ url: 'https://brandless.fast.store/europe/it',
42
+ salesChannel: '1',
43
+ isDefault: true,
44
+ },
45
+ ],
46
+ },
47
+ },
48
+ currencies: {
49
+ USD: { code: 'USD', name: 'US Dollar', symbol: '$' },
50
+ BRL: { code: 'BRL', name: 'Brazilian Real', symbol: 'R$' },
51
+ },
52
+ },
53
+ },
54
+ }))
55
+
56
+ const OTHER_LOCALES = [
57
+ { locale: 'en-US', slug: 'eletronicos' },
58
+ { locale: 'pt-BR', slug: 'eletronicos' },
59
+ { locale: 'it-IT', slug: 'elettronica' },
60
+ ]
61
+
62
+ describe('buildHreflangLinks', () => {
63
+ it('points each locale at its own binding', () => {
64
+ const links = buildHreflangLinks(OTHER_LOCALES, 'en-US')
65
+
66
+ expect(links).toContainEqual({
67
+ hrefLang: 'pt-BR',
68
+ href: 'https://brandless.fast.store/pt-BR/eletronicos',
69
+ })
70
+ expect(links).toContainEqual({
71
+ hrefLang: 'it-IT',
72
+ href: 'https://brandless.fast.store/europe/it/elettronica',
73
+ })
74
+ })
75
+
76
+ // A cluster that does not reference itself is ignored by search engines.
77
+ it('includes the rendered locale itself', () => {
78
+ const links = buildHreflangLinks(OTHER_LOCALES, 'en-US')
79
+
80
+ expect(links).toContainEqual({
81
+ hrefLang: 'en-US',
82
+ href: 'https://brandless.fast.store/en-US/eletronicos',
83
+ })
84
+ })
85
+
86
+ // The store root answers too, but it canonicalizes elsewhere, so pointing
87
+ // x-default at it would send the cluster to a page that disclaims itself.
88
+ it('derives x-default from the default locale binding, not the store root', () => {
89
+ const links = buildHreflangLinks(OTHER_LOCALES, 'en-US')
90
+
91
+ expect(links).toContainEqual({
92
+ hrefLang: 'x-default',
93
+ href: 'https://brandless.fast.store/en-US/eletronicos',
94
+ })
95
+ })
96
+
97
+ it('appends the product suffix when given one', () => {
98
+ const links = buildHreflangLinks(
99
+ [{ locale: 'pt-BR', slug: 'camisa-12' }],
100
+ 'pt-BR',
101
+ '/p'
102
+ )
103
+
104
+ expect(links).toContainEqual({
105
+ hrefLang: 'pt-BR',
106
+ href: 'https://brandless.fast.store/pt-BR/camisa-12/p',
107
+ })
108
+ })
109
+
110
+ it('omits x-default when the default locale has no slug registered', () => {
111
+ const links = buildHreflangLinks(
112
+ [{ locale: 'pt-BR', slug: 'eletronicos' }],
113
+ 'pt-BR'
114
+ )
115
+
116
+ expect(links.some(({ hrefLang }) => hrefLang === 'x-default')).toBe(false)
117
+ })
118
+
119
+ // The catalog resolves the page in it-IT by falling back to another locale's
120
+ // slug, so the shopper sees it, but it-IT registered no translation and is
121
+ // therefore missing from the cluster it would be annotating.
122
+ it('annotates nothing when the rendered locale is not in the cluster', () => {
123
+ const links = buildHreflangLinks(
124
+ [
125
+ { locale: 'en-US', slug: 'eletronicos' },
126
+ { locale: 'pt-BR', slug: 'eletronicos' },
127
+ ],
128
+ 'it-IT'
129
+ )
130
+
131
+ expect(links).toEqual([])
132
+ })
133
+
134
+ // Next only leaves the locale undefined when i18n is off, which means the
135
+ // store has a single locale and it is the default one.
136
+ it('falls back to the default locale when Next reports none', () => {
137
+ const links = buildHreflangLinks(OTHER_LOCALES, undefined)
138
+
139
+ expect(links).toContainEqual({
140
+ hrefLang: 'en-US',
141
+ href: 'https://brandless.fast.store/en-US/eletronicos',
142
+ })
143
+ })
144
+
145
+ it('annotates nothing when there are no alternates', () => {
146
+ expect(buildHreflangLinks(null, 'en-US')).toEqual([])
147
+ expect(buildHreflangLinks([], 'en-US')).toEqual([])
148
+ })
149
+ })