@faststore/core 4.7.1-dev.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.
- package/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +103 -102
- package/@generated/gql.ts +3 -3
- package/@generated/graphql.ts +14 -3
- package/@generated/persisted-documents.json +1 -1
- package/@generated/schema.graphql +12 -1
- package/CHANGELOG.md +6 -0
- package/package.json +6 -6
- package/src/components/templates/LandingPage/LandingPage.tsx +3 -1
- package/src/components/templates/ProductListingPage/ProductListingPage.tsx +18 -7
- package/src/pages/[...slug].tsx +1 -0
- package/src/pages/[slug]/p.tsx +12 -45
- package/src/pages/index.tsx +3 -1
- package/src/sdk/localization/settings.ts +32 -0
- package/src/sdk/localization/useLocalizationConfig.tsx +16 -3
- package/src/utils/localization/hreflang.ts +73 -0
- package/test/sdk/localization/store-url.test.ts +16 -0
- package/test/utils/localization/hreflang.test.ts +149 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.7.0 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 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
|
[33m[STARTED][39m Parse Configuration
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
[32m[COMPLETED][39m Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
|
|
20
20
|
[32m[COMPLETED][39m Generate outputs
|
|
21
21
|
|
|
22
|
-
> @faststore/core@4.7.0 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
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.7.0 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,22 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
Not implemented: navigation to another Document
|
|
9
9
|
Not implemented: navigation to another Document
|
|
10
|
-
|
|
10
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/ui/useScrollRestoration.browser.test.ts [2m([22m[2m43 tests[22m[2m)[22m[33m 1117[2mms[22m[39m
|
|
11
|
+
Browserslist: browsers data (caniuse-lite) is 7 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
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
14
|
-
|
|
15
|
-
[33m[2m✓[22m[39m
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[33m[2m✓[22m[39m
|
|
19
|
-
[33m[2m✓[22m[39m
|
|
20
|
-
[33m[2m✓[22m[39m keeps Confirm disabled
|
|
21
|
-
[33m[2m✓[22m[39m keeps Confirm disabled when the selected contract is filtered out [33m
|
|
22
|
-
|
|
23
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
24
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
25
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
14
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/cards/MyAccountListCards.browser.test.tsx [2m([22m[2m14 tests[22m[2m)[22m[33m 2394[2mms[22m[39m
|
|
15
|
+
[33m[2m✓[22m[39m shows the tab bar and renders Personal cards by default [33m 880[2mms[22m[39m
|
|
16
|
+
[33m[2m✓[22m[39m paginates when there are more than 25 cards [33m 451[2mms[22m[39m
|
|
17
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/ContractSwitcher.browser.test.tsx [2m([22m[2m13 tests[22m[2m)[22m[33m 3084[2mms[22m[39m
|
|
18
|
+
[33m[2m✓[22m[39m shows an error state when the list fails to load [33m 657[2mms[22m[39m
|
|
19
|
+
[33m[2m✓[22m[39m requires selecting a contract before Confirm is enabled, then switches (REQ-03) [33m 469[2mms[22m[39m
|
|
20
|
+
[33m[2m✓[22m[39m keeps Confirm disabled until a different contract is selected [33m 394[2mms[22m[39m
|
|
21
|
+
[33m[2m✓[22m[39m keeps Confirm disabled when switching is unavailable [33m 319[2mms[22m[39m
|
|
22
|
+
[33m[2m✓[22m[39m keeps Confirm disabled when the selected contract is filtered out [33m 390[2mms[22m[39m
|
|
23
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/useBindingSelector.browser.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[32m 228[2mms[22m[39m
|
|
24
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.browser.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 22[2mms[22m[39m
|
|
25
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/recommendations/useRecommendationShelf.browser.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[32m 206[2mms[22m[39m
|
|
26
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 196[2mms[22m[39m
|
|
26
27
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
|
|
27
28
|
[22m[39mError in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
|
|
28
29
|
[90m at node:internal/deps/undici/undici:16416:13[39m
|
|
@@ -49,11 +50,10 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/recommendations/useRecommendationShelf.browser.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[32m 262[2mms[22m[39m
|
|
53
53
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx[2m > [22m[2mRecommendationShelf[2m > [22m[2mrenders nothing when there is an error
|
|
54
54
|
[22m[39mError fetching recommendations [90mundefined[39m boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
|
|
55
55
|
|
|
56
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m7 tests[22m[2m)[22m[
|
|
56
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m7 tests[22m[2m)[22m[32m 241[2mms[22m[39m
|
|
57
57
|
[90mstderr[2m | test/pages/password-protection.browser.test.tsx[2m > [22m[2mPasswordProtectionLogin[2m > [22m[2mposts the password and redirects to the requested return path
|
|
58
58
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
59
59
|
|
|
@@ -187,6 +187,8 @@ act(() => {
|
|
|
187
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
|
|
188
188
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
189
189
|
|
|
190
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawer.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[33m 1055[2mms[22m[39m
|
|
191
|
+
[33m[2m✓[22m[39m renders the menu view with contract details and manage footer [33m 706[2mms[22m[39m
|
|
190
192
|
[90mstderr[2m | test/pages/password-protection.browser.test.tsx[2m > [22m[2mPasswordProtectionLogin[2m > [22m[2mshows service unavailable when the response is not a login payload
|
|
191
193
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
192
194
|
|
|
@@ -283,12 +285,10 @@ act(() => {
|
|
|
283
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
|
|
284
286
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
285
287
|
|
|
286
|
-
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m
|
|
287
|
-
[33m[2m✓[22m[39m renders the password protection form [33m
|
|
288
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
289
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
290
|
-
[33m[2m✓[22m[39m renders the menu view with contract details and manage footer [33m 329[2mms[22m[39m
|
|
291
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/changeContractToken.browser.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 24[2mms[22m[39m
|
|
288
|
+
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m 928[2mms[22m[39m
|
|
289
|
+
[33m[2m✓[22m[39m renders the password protection form [33m 470[2mms[22m[39m
|
|
290
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
291
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/changeContractToken.browser.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
292
292
|
[90mstderr[2m | test/components/cms/RenderSections.browser.test.tsx
|
|
293
293
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
294
294
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -297,7 +297,6 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
297
297
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
298
298
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
299
299
|
|
|
300
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/cms/RenderSections.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 80[2mms[22m[39m
|
|
301
300
|
[90mstderr[2m | test/sdk/localization/store-url.browser.test.ts
|
|
302
301
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
303
302
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -307,10 +306,11 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
307
306
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
308
307
|
|
|
309
308
|
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/store-url.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
310
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useAvailableContracts.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
311
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/
|
|
312
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
313
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
309
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useAvailableContracts.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 34[2mms[22m[39m
|
|
310
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/cms/RenderSections.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 97[2mms[22m[39m
|
|
311
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/templates/SearchPage/SearchWrapper.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 36[2mms[22m[39m
|
|
312
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 134[2mms[22m[39m
|
|
313
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useSwitchContract.browser.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 46[2mms[22m[39m
|
|
314
314
|
[90mstderr[2m | test/components/account/doLogout.browser.test.ts
|
|
315
315
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
316
316
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -319,9 +319,10 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
319
319
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
320
320
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
321
321
|
|
|
322
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
323
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/ui/LocalizationButton.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
324
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/sections/Account/AccountListCards.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
322
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
323
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/ui/LocalizationButton.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 49[2mms[22m[39m
|
|
324
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/sections/Account/AccountListCards.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 91[2mms[22m[39m
|
|
325
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerHeader.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 288[2mms[22m[39m
|
|
325
326
|
[90mstderr[2m | test/sdk/account/clearPersistedSessionState.browser.test.ts
|
|
326
327
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
327
328
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -330,15 +331,12 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
330
331
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
331
332
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
332
333
|
|
|
333
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
334
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
335
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
336
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
337
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
338
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m
|
|
339
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/skeletons/ProductGridSkeleton.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 31[2mms[22m[39m
|
|
340
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/password-protection-service.test.ts [2m([22m[2m22 tests[22m[2m)[22m[32m 34[2mms[22m[39m
|
|
341
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 13[2mms[22m[39m
|
|
334
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerFooter.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[33m 307[2mms[22m[39m
|
|
335
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/clearPersistedSessionState.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
336
|
+
[32m✓[39m [30m[46m browser [49m[39m test/utils/isLocalHost.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
337
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
338
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/skeletons/ProductGridSkeleton.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 37[2mms[22m[39m
|
|
339
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
342
340
|
[90mstderr[2m | test/sdk/account/refreshToken.browser.test.ts
|
|
343
341
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
344
342
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -347,10 +345,12 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
347
345
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
348
346
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
349
347
|
|
|
350
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
351
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
352
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
353
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/
|
|
348
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
349
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/password-protection-service.test.ts [2m([22m[2m22 tests[22m[2m)[22m[32m 35[2mms[22m[39m
|
|
350
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
351
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/useBindingSelector.test.tsx [2m([22m[2m22 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
352
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/unlock.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 191[2mms[22m[39m
|
|
353
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/bindingSelector.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
354
354
|
[90mstderr[2m | test/components/search/SearchInputComponent.test.tsx
|
|
355
355
|
[22m[39mWarning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
|
|
356
356
|
|
|
@@ -414,18 +414,19 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
414
414
|
at Suspense
|
|
415
415
|
at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
|
|
416
416
|
|
|
417
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
418
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m
|
|
419
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m
|
|
417
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 85[2mms[22m[39m
|
|
418
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 51[2mms[22m[39m
|
|
419
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 17[2mms[22m[39m
|
|
420
420
|
[90mstderr[2m | test/pages/pvt/account/cards/index.test.ts[2m > [22m[2mCards page getServerSideProps[2m > [22m[2mredirects to /pvt/account/403 when the query fails with 401/403
|
|
421
421
|
[22m[39m{ extensions: { status: [33m401[39m } }
|
|
422
422
|
|
|
423
423
|
[90mstderr[2m | test/pages/pvt/account/cards/index.test.ts[2m > [22m[2mCards page getServerSideProps[2m > [22m[2mrenders the error state (no redirect) when the query fails with a non-auth status
|
|
424
424
|
[22m[39m{ extensions: { status: [33m502[39m } }
|
|
425
425
|
|
|
426
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/pvt/account/cards/index.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
427
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m
|
|
428
|
-
[32m✓[39m [30m[43m node [49m[39m test/experimental/myAccountCmsServerSideProps.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
426
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/pvt/account/cards/index.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
427
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 27[2mms[22m[39m
|
|
428
|
+
[32m✓[39m [30m[43m node [49m[39m test/experimental/myAccountCmsServerSideProps.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
429
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m 370[2mms[22m[39m
|
|
429
430
|
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates the upstream 400 from a wrapped BadRequestError instead of 500
|
|
430
431
|
[22m[39mGraphql execution returned with error: [
|
|
431
432
|
{
|
|
@@ -474,7 +475,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
474
475
|
|
|
475
476
|
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql request handling[2m > [22m[2mmaps BadRequestError thrown by execute to its status
|
|
476
477
|
[22m[39mSomething unexpected occurred querying Graphql endpoint:
|
|
477
|
-
|
|
478
|
+
E [FastStoreError]: malformed
|
|
478
479
|
at [90m/home/runner/work/faststore/faststore/packages/core/[39mtest/pages/api/graphql.test.ts:190:31
|
|
479
480
|
at [90m/home/runner/work/faststore/faststore/packages/core/[39mtest/pages/api/graphql.test.ts:196:39
|
|
480
481
|
at file:///home/runner/work/faststore/faststore/node_modules/[4m.pnpm[24m/@vitest+runner@4.0.18/node_modules/[4m@vitest/runner[24m/dist/index.js:1024:60
|
|
@@ -490,7 +491,7 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
490
491
|
|
|
491
492
|
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql request handling[2m > [22m[2mmaps UnauthorizedError thrown by execute to its status
|
|
492
493
|
[22m[39mSomething unexpected occurred querying Graphql endpoint:
|
|
493
|
-
|
|
494
|
+
ie [FastStoreError]: expired
|
|
494
495
|
at [90m/home/runner/work/faststore/faststore/packages/core/[39mtest/pages/api/graphql.test.ts:191:33
|
|
495
496
|
at [90m/home/runner/work/faststore/faststore/packages/core/[39mtest/pages/api/graphql.test.ts:196:39
|
|
496
497
|
at file:///home/runner/work/faststore/faststore/node_modules/[4m.pnpm[24m/@vitest+runner@4.0.18/node_modules/[4m@vitest/runner[24m/dist/index.js:1024:60
|
|
@@ -518,68 +519,68 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
518
519
|
at file:///home/runner/work/faststore/faststore/node_modules/[4m.pnpm[24m/@vitest+runner@4.0.18/node_modules/[4m@vitest/runner[24m/dist/index.js:1653:37
|
|
519
520
|
at Traces.$ (file:///home/runner/work/faststore/faststore/node_modules/[4m.pnpm[24m/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/[4mvitest[24m/dist/chunks/traces.CCmnQaNT.js:142:27)
|
|
520
521
|
|
|
521
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m
|
|
522
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m 415[2mms[22m[39m
|
|
523
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m 820[2mms[22m[39m
|
|
524
|
-
[33m[2m✓[22m[39m navigates with the search term on blur [33m 364[2mms[22m[39m
|
|
525
|
-
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m 361[2mms[22m[39m
|
|
522
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m 36[2mms[22m[39m
|
|
526
523
|
[90mstderr[2m | test/sdk/account/useSetPassword.test.ts[2m > [22m[2museSetPassword[2m > [22m[2mmaps wrongcredentials error responses to the existing message
|
|
527
524
|
[22m[39mSet password request failed: Bad Request
|
|
528
525
|
|
|
529
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/useSetPassword.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
530
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
531
|
-
|
|
526
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/useSetPassword.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 34[2mms[22m[39m
|
|
527
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m 835[2mms[22m[39m
|
|
528
|
+
[33m[2m✓[22m[39m navigates with the search term on blur [33m 363[2mms[22m[39m
|
|
529
|
+
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m 359[2mms[22m[39m
|
|
530
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryOperation.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 45[2mms[22m[39m
|
|
531
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/quoteFilters.test.ts [2m([22m[2m23 tests[22m[2m)[22m[32m 37[2mms[22m[39m
|
|
532
532
|
[32m✓[39m [30m[43m node [49m[39m test/utils/multipleTemplates.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
533
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/localizeRedirectDestination.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m
|
|
534
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
535
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
536
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
537
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
538
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
539
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
540
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
541
|
-
|
|
542
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
543
|
-
|
|
544
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
533
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/localizeRedirectDestination.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
534
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/hreflang.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
535
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getMyAccountRoutes.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
536
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryUpload.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 50[2mms[22m[39m
|
|
537
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/redirects/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 45[2mms[22m[39m
|
|
538
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesFilterSlider.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 163[2mms[22m[39m
|
|
539
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/productJsonLd.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
540
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.localization.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
541
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntry.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 39[2mms[22m[39m
|
|
542
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 953[2mms[22m[39m
|
|
543
|
+
[33m[2m✓[22m[39m should handle options and execute [33m 453[2mms[22m[39m
|
|
544
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/session/syncSalesChannelFromOrderForm.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
545
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesSelectedTags.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 127[2mms[22m[39m
|
|
545
546
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/useStartRecommendationSession.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 48[2mms[22m[39m
|
|
546
|
-
[32m✓[39m [30m[43m node [49m[39m test/proxy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 32[2mms[22m[39m
|
|
547
547
|
[32m✓[39m [30m[43m node [49m[39m test/server/webops-api.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 18[2mms[22m[39m
|
|
548
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
549
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
548
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 20[2mms[22m[39m
|
|
549
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderFormItems.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 45[2mms[22m[39m
|
|
550
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/cart/waitForSessionValidated.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
551
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/store-url.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
550
552
|
[32m✓[39m [30m[43m node [49m[39m test/server/cms/myAccountDefaultSections.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
551
553
|
[32m✓[39m [30m[43m node [49m[39m test/server/content/service.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
552
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/productJsonLdRendering.test.tsx [2m([22m[2m4 tests[22m[2m)[22m[32m
|
|
554
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/productJsonLdRendering.test.tsx [2m([22m[2m4 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
555
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/global.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
553
556
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/refreshToken.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
554
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
555
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
556
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
557
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
558
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/getRequestHostname.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
559
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/preview.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
560
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/rootSpan.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 50[2mms[22m[39m
|
|
561
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
562
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/account/Layout.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 77[2mms[22m[39m
|
|
557
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/auth/useAuth.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 36[2mms[22m[39m
|
|
558
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesStatusSelector.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 167[2mms[22m[39m
|
|
559
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/rootSpan.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 46[2mms[22m[39m
|
|
560
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/getRequestHostname.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
563
561
|
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInput.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
564
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
565
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/
|
|
566
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
567
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
568
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
569
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
570
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
571
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/
|
|
562
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/preview.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
563
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
564
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/Layout.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 54[2mms[22m[39m
|
|
565
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/vrn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
566
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/index.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
567
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/retry.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
568
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getB2BSessionClaims.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
569
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/unlockResponse.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
570
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/useRecommendations.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
571
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/useRecommendationUserId.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 197[2mms[22m[39m
|
|
572
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/isLocalHost.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
572
573
|
[32m✓[39m [30m[43m node [49m[39m test/utils/userOrderStatus.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
573
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/account/OrganizationDrawerBody.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
574
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/OrganizationDrawerBody.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 54[2mms[22m[39m
|
|
574
575
|
[32m✓[39m [30m[43m node [49m[39m test/utils/getCookie.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
575
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/utils.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
576
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
577
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
578
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/clearPersistedSessionState.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
576
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/utils.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
577
|
+
[32m✓[39m [30m[43m node [49m[39m test/instrumentation.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
578
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/auth/ProfileChallenge.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 42[2mms[22m[39m
|
|
579
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/clearPersistedSessionState.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
579
580
|
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuoteStatusBadge.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 25[2mms[22m[39m
|
|
580
581
|
|
|
581
|
-
[2m Test Files [22m [1m[
|
|
582
|
-
[2m Tests [22m [1m[
|
|
583
|
-
[2m Start at [22m
|
|
584
|
-
[2m Duration [22m 45.
|
|
582
|
+
[2m Test Files [22m [1m[32m94 passed[39m[22m[90m (94)[39m
|
|
583
|
+
[2m Tests [22m [1m[32m753 passed[39m[22m[90m (753)[39m
|
|
584
|
+
[2m Start at [22m 23:40:57
|
|
585
|
+
[2m Duration [22m 45.98s[2m (transform 12.14s, setup 0ms, import 40.59s, tests 14.88s, environment 57.52s)[22m
|
|
585
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
|
*/
|
package/@generated/graphql.ts
CHANGED
|
@@ -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
|
|
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":"
|
|
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
|
-
"
|
|
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
|
|
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,12 @@
|
|
|
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
|
+
|
|
6
12
|
## [4.7.1-dev.0](https://github.com/vtex/faststore/compare/v4.7.0...v4.7.1-dev.0) (2026-08-31)
|
|
7
13
|
|
|
8
14
|
**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.1-dev.
|
|
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/api": "4.7.1-dev.
|
|
75
|
-
"@faststore/diagnostics": "4.7.1-dev.
|
|
76
|
-
"@faststore/
|
|
77
|
-
"@faststore/sdk": "4.7.1-dev.
|
|
78
|
-
"@faststore/
|
|
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
|
|
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 =
|
|
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,
|
package/src/pages/[...slug].tsx
CHANGED
package/src/pages/[slug]/p.tsx
CHANGED
|
@@ -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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/pages/index.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
+
})
|