@faststore/core 4.7.0-dev.5 → 4.7.0-dev.7
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 +97 -94
- package/CHANGELOG.md +12 -0
- package/package.json +6 -6
- package/src/pages/[slug]/p.tsx +9 -15
- package/src/sdk/redirects/index.ts +20 -2
- package/src/utils/productJsonLd.ts +71 -0
- package/test/sdk/redirects/index.test.ts +126 -0
- package/test/utils/productJsonLd.test.ts +126 -0
- package/test/utils/productJsonLdRendering.test.tsx +74 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.7.0-dev.
|
|
2
|
+
> @faststore/core@4.7.0-dev.6 generate /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> pnpm run gen-types && pnpm run cache-graphql
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @faststore/core@4.7.0-dev.
|
|
6
|
+
> @faststore/core@4.7.0-dev.6 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-dev.
|
|
22
|
+
> @faststore/core@4.7.0-dev.6 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-dev.
|
|
2
|
+
> @faststore/core@4.7.0-dev.6 test /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
@@ -7,20 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
Not implemented: navigation to another Document
|
|
9
9
|
Not implemented: navigation to another Document
|
|
10
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/ui/useScrollRestoration.browser.test.ts [2m([22m[2m43 tests[22m[2m)[22m[33m
|
|
10
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/ui/useScrollRestoration.browser.test.ts [2m([22m[2m43 tests[22m[2m)[22m[33m 1180[2mms[22m[39m
|
|
11
11
|
Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
|
|
12
12
|
npx update-browserslist-db@latest
|
|
13
13
|
Why you should do it regularly: https://github.com/browserslist/update-db#readme
|
|
14
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 260[2mms[22m[39m
|
|
15
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/ContractSwitcher.browser.test.tsx [2m([22m[2m13 tests[22m[2m)[22m[33m 2965[2mms[22m[39m
|
|
16
|
-
[33m[2m✓[22m[39m shows an error state when the list fails to load [33m 863[2mms[22m[39m
|
|
17
|
-
[33m[2m✓[22m[39m requires selecting a contract before Confirm is enabled, then switches (REQ-03) [33m 427[2mms[22m[39m
|
|
18
|
-
[33m[2m✓[22m[39m keeps Confirm disabled when switching is unavailable [33m 372[2mms[22m[39m
|
|
19
14
|
[32m✓[39m [30m[46m browser [49m[39m test/components/account/cards/MyAccountListCards.browser.test.tsx [2m([22m[2m14 tests[22m[2m)[22m[33m 2735[2mms[22m[39m
|
|
20
|
-
[33m[2m✓[22m[39m shows the tab bar and renders Personal cards by default [33m
|
|
21
|
-
[33m[2m✓[22m[39m paginates when there are more than 25 cards [33m
|
|
22
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
23
|
-
|
|
15
|
+
[33m[2m✓[22m[39m shows the tab bar and renders Personal cards by default [33m 986[2mms[22m[39m
|
|
16
|
+
[33m[2m✓[22m[39m paginates when there are more than 25 cards [33m 484[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 2741[2mms[22m[39m
|
|
18
|
+
[33m[2m✓[22m[39m shows an error state when the list fails to load [33m 834[2mms[22m[39m
|
|
19
|
+
[33m[2m✓[22m[39m requires selecting a contract before Confirm is enabled, then switches (REQ-03) [33m 480[2mms[22m[39m
|
|
20
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 286[2mms[22m[39m
|
|
21
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.browser.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 28[2mms[22m[39m
|
|
22
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/recommendations/useRecommendationShelf.browser.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[32m 128[2mms[22m[39m
|
|
24
23
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
|
|
25
24
|
[22m[39mError in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
|
|
26
25
|
[90m at node:internal/deps/undici/undici:16416:13[39m
|
|
@@ -50,7 +49,7 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
|
|
|
50
49
|
[90mstderr[2m | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx[2m > [22m[2mRecommendationShelf[2m > [22m[2mrenders nothing when there is an error
|
|
51
50
|
[22m[39mError fetching recommendations [90mundefined[39m boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
|
|
52
51
|
|
|
53
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m7 tests[22m[2m)[22m[32m
|
|
52
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx [2m([22m[2m7 tests[22m[2m)[22m[32m 234[2mms[22m[39m
|
|
54
53
|
[90mstderr[2m | test/pages/password-protection.browser.test.tsx[2m > [22m[2mPasswordProtectionLogin[2m > [22m[2mposts the password and redirects to the requested return path
|
|
55
54
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
56
55
|
|
|
@@ -136,6 +135,7 @@ act(() => {
|
|
|
136
135
|
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
|
|
137
136
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
138
137
|
|
|
138
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 32[2mms[22m[39m
|
|
139
139
|
[90mstderr[2m | test/pages/password-protection.browser.test.tsx[2m > [22m[2mPasswordProtectionLogin[2m > [22m[2muses the fallback invalid-password message when API omits an error
|
|
140
140
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
141
141
|
|
|
@@ -184,7 +184,8 @@ act(() => {
|
|
|
184
184
|
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
|
|
185
185
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
186
186
|
|
|
187
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
187
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawer.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[33m 1658[2mms[22m[39m
|
|
188
|
+
[33m[2m✓[22m[39m renders the menu view with contract details and manage footer [33m 988[2mms[22m[39m
|
|
188
189
|
[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
|
|
189
190
|
[22m[39mWarning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
|
|
190
191
|
|
|
@@ -281,12 +282,10 @@ act(() => {
|
|
|
281
282
|
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
|
|
282
283
|
at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
|
|
283
284
|
|
|
284
|
-
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m
|
|
285
|
-
[33m[2m✓[22m[39m renders the password protection form [33m
|
|
286
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
287
|
-
|
|
288
|
-
[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
|
|
289
|
-
[90mstderr[2m | test/components/cms/RenderSections.browser.test.tsx
|
|
285
|
+
[32m✓[39m [30m[46m browser [49m[39m test/pages/password-protection.browser.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[33m 1773[2mms[22m[39m
|
|
286
|
+
[33m[2m✓[22m[39m renders the password protection form [33m 871[2mms[22m[39m
|
|
287
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/changeContractToken.browser.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 33[2mms[22m[39m
|
|
288
|
+
[90mstderr[2m | test/sdk/localization/store-url.browser.test.ts
|
|
290
289
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
291
290
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
292
291
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:120:9[90m)[39m
|
|
@@ -294,7 +293,8 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
294
293
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
295
294
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
296
295
|
|
|
297
|
-
[
|
|
296
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/store-url.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
297
|
+
[90mstderr[2m | test/components/cms/RenderSections.browser.test.tsx
|
|
298
298
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
299
299
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
300
300
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:120:9[90m)[39m
|
|
@@ -302,12 +302,11 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
302
302
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
303
303
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
304
304
|
|
|
305
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
306
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/cms/RenderSections.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 81[2mms[22m[39m
|
|
305
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/cms/RenderSections.browser.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 91[2mms[22m[39m
|
|
307
306
|
[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
|
|
308
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/templates/SearchPage/SearchWrapper.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
309
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
310
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/
|
|
307
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/templates/SearchPage/SearchWrapper.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
|
308
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 129[2mms[22m[39m
|
|
309
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/useSwitchContract.browser.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
|
311
310
|
[90mstderr[2m | test/components/account/doLogout.browser.test.ts
|
|
312
311
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
313
312
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -316,9 +315,10 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
316
315
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
317
316
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
318
317
|
|
|
319
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
320
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/sections/Account/AccountListCards.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
321
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerHeader.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[33m
|
|
318
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/doLogout.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
319
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/sections/Account/AccountListCards.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 98[2mms[22m[39m
|
|
320
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerHeader.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[33m 328[2mms[22m[39m
|
|
321
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/account/OrganizationDrawerFooter.browser.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[33m 313[2mms[22m[39m
|
|
322
322
|
[90mstderr[2m | test/sdk/account/clearPersistedSessionState.browser.test.ts
|
|
323
323
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
324
324
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -327,10 +327,10 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
327
327
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
328
328
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
329
329
|
|
|
330
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/clearPersistedSessionState.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
331
|
-
[32m✓[39m [30m[46m browser [49m[39m test/utils/isLocalHost.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
332
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
333
|
-
[32m✓[39m [30m[
|
|
330
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/clearPersistedSessionState.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
331
|
+
[32m✓[39m [30m[46m browser [49m[39m test/utils/isLocalHost.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
332
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
333
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m 32[2mms[22m[39m
|
|
334
334
|
[90mstderr[2m | test/sdk/account/refreshToken.browser.test.ts
|
|
335
335
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
336
336
|
at waitForSessionValidated [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/waitForSessionValidated.ts:16:32[90m)[39m
|
|
@@ -339,14 +339,13 @@ This ensures that you're testing the behavior the user would see in the browser.
|
|
|
339
339
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
|
|
340
340
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
341
341
|
|
|
342
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
343
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m 21[2mms[22m[39m
|
|
344
|
-
[32m✓[39m [30m[46m browser [49m[39m test/components/skeletons/ProductGridSkeleton.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 40[2mms[22m[39m
|
|
345
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
346
342
|
[32m✓[39m [30m[43m node [49m[39m test/server/password-protection-service.test.ts [2m([22m[2m22 tests[22m[2m)[22m[32m 38[2mms[22m[39m
|
|
347
|
-
[32m✓[39m [30m[
|
|
348
|
-
[32m✓[39m [30m[
|
|
349
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
343
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/account/refreshToken.browser.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
344
|
+
[32m✓[39m [30m[46m browser [49m[39m test/components/skeletons/ProductGridSkeleton.browser.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
345
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
346
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/unlock.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 229[2mms[22m[39m
|
|
347
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/useBindingSelector.test.tsx [2m([22m[2m22 tests[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
348
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/bindingSelector.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
350
349
|
[90mstderr[2m | test/components/search/SearchInputComponent.test.tsx
|
|
351
350
|
[22m[39mWarning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
|
|
352
351
|
|
|
@@ -410,19 +409,19 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
410
409
|
at Suspense
|
|
411
410
|
at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
|
|
412
411
|
|
|
413
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
414
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m
|
|
415
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m
|
|
412
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInputComponent.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 82[2mms[22m[39m
|
|
413
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 58[2mms[22m[39m
|
|
414
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
416
415
|
[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
|
|
417
416
|
[22m[39m{ extensions: { status: [33m401[39m } }
|
|
418
417
|
|
|
419
418
|
[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
|
|
420
419
|
[22m[39m{ extensions: { status: [33m502[39m } }
|
|
421
420
|
|
|
422
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/pvt/account/cards/index.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
423
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m
|
|
424
|
-
[32m✓[39m [30m[43m node [49m[39m test/experimental/myAccountCmsServerSideProps.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
425
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m
|
|
421
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/pvt/account/cards/index.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
422
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 22[2mms[22m[39m
|
|
423
|
+
[32m✓[39m [30m[43m node [49m[39m test/experimental/myAccountCmsServerSideProps.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
424
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotesTable.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[33m 451[2mms[22m[39m
|
|
426
425
|
[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
|
|
427
426
|
[22m[39mGraphql execution returned with error: [
|
|
428
427
|
{
|
|
@@ -515,64 +514,68 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
|
|
|
515
514
|
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
|
|
516
515
|
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)
|
|
517
516
|
|
|
518
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m
|
|
519
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m
|
|
520
|
-
[33m[2m✓[22m[39m navigates with the search term on blur [33m
|
|
521
|
-
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m
|
|
517
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m 32[2mms[22m[39m
|
|
518
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountListQuotes.test.tsx [2m([22m[2m9 tests[22m[2m)[22m[33m 811[2mms[22m[39m
|
|
519
|
+
[33m[2m✓[22m[39m navigates with the search term on blur [33m 361[2mms[22m[39m
|
|
520
|
+
[33m[2m✓[22m[39m navigates without a label param when the search is cleared [33m 359[2mms[22m[39m
|
|
522
521
|
[90mstderr[2m | test/sdk/account/useSetPassword.test.ts[2m > [22m[2museSetPassword[2m > [22m[2mmaps wrongcredentials error responses to the existing message
|
|
523
522
|
[22m[39mSet password request failed: Bad Request
|
|
524
523
|
|
|
525
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/useSetPassword.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
526
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
527
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
528
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
524
|
+
[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
|
|
525
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryOperation.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
|
526
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/quoteFilters.test.ts [2m([22m[2m23 tests[22m[2m)[22m[32m 41[2mms[22m[39m
|
|
527
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/multipleTemplates.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
529
528
|
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/localizeRedirectDestination.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
530
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getMyAccountRoutes.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m
|
|
531
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryUpload.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
532
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
533
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
534
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
535
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
536
|
-
|
|
529
|
+
[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
|
|
530
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntryUpload.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 45[2mms[22m[39m
|
|
531
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/redirects/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 41[2mms[22m[39m
|
|
532
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesFilterSlider.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 182[2mms[22m[39m
|
|
533
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/productJsonLd.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
|
534
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.localization.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 56[2mms[22m[39m
|
|
535
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/orderEntry/useOrderEntry.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 46[2mms[22m[39m
|
|
536
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 1302[2mms[22m[39m
|
|
537
|
+
[33m[2m✓[22m[39m should return a valid merged GraphQL schema [33m 334[2mms[22m[39m
|
|
538
|
+
[33m[2m✓[22m[39m should handle options and execute [33m 689[2mms[22m[39m
|
|
537
539
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/session/syncSalesChannelFromOrderForm.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
538
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesSelectedTags.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m
|
|
540
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesSelectedTags.test.tsx [2m([22m[2m8 tests[22m[2m)[22m[32m 118[2mms[22m[39m
|
|
539
541
|
[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
|
|
540
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
541
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
542
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
543
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
544
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/
|
|
545
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
546
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/refreshToken.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
547
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
548
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
549
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/
|
|
550
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
542
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/webops-api.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
543
|
+
[32m✓[39m [30m[43m node [49m[39m test/proxy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 17[2mms[22m[39m
|
|
544
|
+
[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
|
|
545
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/myAccountDefaultSections.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
546
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/content/service.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
547
|
+
[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
|
|
548
|
+
[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
|
|
549
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/productJsonLdRendering.test.tsx [2m([22m[2m4 tests[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
550
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/global.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
551
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/store-url.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
552
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/auth/useAuth.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 31[2mms[22m[39m
|
|
553
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuotesStatusSelector.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 163[2mms[22m[39m
|
|
551
554
|
[32m✓[39m [30m[43m node [49m[39m test/utils/getRequestHostname.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
552
|
-
[32m✓[39m [30m[43m node [49m[39m test/pages/api/preview.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
553
|
-
[32m✓[39m [30m[43m node [49m[39m test/server/rootSpan.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m
|
|
554
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m
|
|
555
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/
|
|
556
|
-
[32m✓[39m [30m[43m node [49m[39m test/components/
|
|
557
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/vrn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
558
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/retry.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
555
|
+
[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
|
|
556
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/rootSpan.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 49[2mms[22m[39m
|
|
557
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 11[2mms[22m[39m
|
|
558
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/search/SearchInput.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
559
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/Layout.test.tsx [2m([22m[2m6 tests[22m[2m)[22m[32m 76[2mms[22m[39m
|
|
560
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/vrn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
561
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/retry.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
559
562
|
[32m✓[39m [30m[43m node [49m[39m test/server/cms/index.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
560
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getB2BSessionClaims.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m
|
|
561
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/unlockResponse.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m
|
|
563
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/getB2BSessionClaims.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
564
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/unlockResponse.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
562
565
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/useRecommendations.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
563
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/useRecommendationUserId.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m
|
|
566
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/recommendations/useRecommendationUserId.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 192[2mms[22m[39m
|
|
567
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/account/OrganizationDrawerBody.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 52[2mms[22m[39m
|
|
564
568
|
[32m✓[39m [30m[43m node [49m[39m test/utils/isLocalHost.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
|
565
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
566
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/userOrderStatus.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
569
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/userOrderStatus.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
567
570
|
[32m✓[39m [30m[43m node [49m[39m test/utils/getCookie.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
571
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/auth/ProfileChallenge.test.tsx [2m([22m[2m3 tests[22m[2m)[22m[32m 44[2mms[22m[39m
|
|
568
572
|
[32m✓[39m [30m[43m node [49m[39m test/sdk/analytics/utils.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
569
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
570
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
571
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/clearPersistedSessionState.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
[2m
|
|
575
|
-
[2m
|
|
576
|
-
[2m
|
|
577
|
-
[2m Duration [22m 45.95s[2m (transform 12.76s, setup 0ms, import 41.15s, tests 16.16s, environment 54.47s)[22m
|
|
573
|
+
[32m✓[39m [30m[43m node [49m[39m test/instrumentation.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
574
|
+
[32m✓[39m [30m[43m node [49m[39m test/components/quotes/MyAccountQuoteStatusBadge.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 28[2mms[22m[39m
|
|
575
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/account/clearPersistedSessionState.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
576
|
+
|
|
577
|
+
[2m Test Files [22m [1m[32m91 passed[39m[22m[90m (91)[39m
|
|
578
|
+
[2m Tests [22m [1m[32m730 passed[39m[22m[90m (730)[39m
|
|
579
|
+
[2m Start at [22m 17:35:39
|
|
580
|
+
[2m Duration [22m 45.76s[2m (transform 13.02s, setup 0ms, import 41.25s, tests 16.71s, environment 55.14s)[22m
|
|
578
581
|
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [4.7.0-dev.7](https://github.com/vtex/faststore/compare/v4.7.0-dev.6...v4.7.0-dev.7) (2026-08-28)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **core:** resolve redirect matcher module explicitly ([#3467](https://github.com/vtex/faststore/issues/3467)) ([7acecf0](https://github.com/vtex/faststore/commit/7acecf03604842db166de7c53038065934c91154)), closes [#1450148](https://github.com/vtex/faststore/issues/1450148) [#3468](https://github.com/vtex/faststore/issues/3468)
|
|
11
|
+
|
|
12
|
+
# [4.7.0-dev.6](https://github.com/vtex/faststore/compare/v4.7.0-dev.5...v4.7.0-dev.6) (2026-08-26)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- make PDP product JSON-LD Schema.org compliant ([#3465](https://github.com/vtex/faststore/issues/3465)) ([0c09f15](https://github.com/vtex/faststore/commit/0c09f15456d3490602970ce0b4dff6b3c76a4f9c)), closes [#1449246](https://github.com/vtex/faststore/issues/1449246)
|
|
17
|
+
|
|
6
18
|
# [4.7.0-dev.5](https://github.com/vtex/faststore/compare/v4.7.0-dev.4...v4.7.0-dev.5) (2026-08-25)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "4.7.0-dev.
|
|
3
|
+
"version": "4.7.0-dev.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"style-loader": "^3.3.1",
|
|
72
72
|
"swr": "^2.2.5",
|
|
73
73
|
"use-sync-external-store": "^1.6.0",
|
|
74
|
-
"@faststore/api": "4.7.0-dev.
|
|
75
|
-
"@faststore/diagnostics": "4.7.0-dev.
|
|
76
|
-
"@faststore/lighthouse": "4.7.0-dev.
|
|
77
|
-
"@faststore/sdk": "4.7.0-dev.
|
|
78
|
-
"@faststore/ui": "4.7.0-dev.
|
|
74
|
+
"@faststore/api": "4.7.0-dev.7",
|
|
75
|
+
"@faststore/diagnostics": "4.7.0-dev.7",
|
|
76
|
+
"@faststore/lighthouse": "4.7.0-dev.7",
|
|
77
|
+
"@faststore/sdk": "4.7.0-dev.7",
|
|
78
|
+
"@faststore/ui": "4.7.0-dev.7"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@cypress/code-coverage": "^3.12.1",
|
package/src/pages/[slug]/p.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import { useSession } from 'src/sdk/session'
|
|
|
28
28
|
import { execute } from 'src/server'
|
|
29
29
|
import { getComponentKey } from 'src/utils/cms'
|
|
30
30
|
import { getChannelForLocale } from 'src/utils/localization/bindingPaths'
|
|
31
|
+
import { toProductJsonLdOffer } from 'src/utils/productJsonLd'
|
|
31
32
|
|
|
32
33
|
import storeConfig from 'discovery.config'
|
|
33
34
|
import {
|
|
@@ -328,11 +329,13 @@ function Page({
|
|
|
328
329
|
description={description}
|
|
329
330
|
brand={product.brand.name}
|
|
330
331
|
sku={product.sku}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
// Spread conditionally so an unregistered identifier is omitted rather
|
|
333
|
+
// than published as "": next-seo passes these through untouched.
|
|
334
|
+
{...(product.gtin && { gtin: product.gtin })}
|
|
335
|
+
{...(product.mpn && { mpn: product.mpn })}
|
|
336
|
+
{...(product.releaseDate && { releaseDate: product.releaseDate })}
|
|
334
337
|
images={product.image.map((img) => img.url)} // Somehow, Google does not understand this valid Schema.org schema, so we need to do conversions
|
|
335
|
-
offers
|
|
338
|
+
{...(offers && { offers })}
|
|
336
339
|
{...(itemListElements.length !== 0 && {
|
|
337
340
|
category: itemListElements[0].name,
|
|
338
341
|
})}
|
|
@@ -508,19 +511,10 @@ export const getStaticProps: GetStaticProps<
|
|
|
508
511
|
|
|
509
512
|
const meta = { title, description, canonical }
|
|
510
513
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (data.product.offers.offers.length > 0) {
|
|
514
|
-
const { listPrice, ...offerData } = data.product.offers.offers[0]
|
|
515
|
-
|
|
516
|
-
offer = offerData
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const offers = {
|
|
520
|
-
...offer,
|
|
514
|
+
const offers = toProductJsonLdOffer(data.product.offers.offers[0], {
|
|
521
515
|
priceCurrency: data.product.offers.priceCurrency,
|
|
522
516
|
url: canonical,
|
|
523
|
-
}
|
|
517
|
+
})
|
|
524
518
|
|
|
525
519
|
const globalSectionsResult = injectGlobalSections({
|
|
526
520
|
globalSections,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import storeConfig from 'discovery.config'
|
|
2
|
-
import { matcher } from 'src/customizations/src/redirects'
|
|
2
|
+
import { matcher } from 'src/customizations/src/redirects/index'
|
|
3
3
|
|
|
4
4
|
type GetRedirectArgs = {
|
|
5
5
|
pathname: string
|
|
@@ -19,6 +19,23 @@ const PERMANENT_STATUS = 308
|
|
|
19
19
|
|
|
20
20
|
const ASSET_FILE_REGEX = /\.(js|css|png|jpg|jpeg|svg|gif|webp|ico|json|map)$/i
|
|
21
21
|
|
|
22
|
+
let hasWarnedInvalidMatcher = false
|
|
23
|
+
|
|
24
|
+
function resolveLocalMatch(pathname: string) {
|
|
25
|
+
if (typeof matcher === 'function') {
|
|
26
|
+
return matcher({ pathname })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!hasWarnedInvalidMatcher) {
|
|
30
|
+
hasWarnedInvalidMatcher = true
|
|
31
|
+
console.warn(
|
|
32
|
+
`[redirects] Expected src/redirects to export a \`matcher\` function, got ${typeof matcher}. Falling back to the platform redirect lookup.`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
export async function getRedirect({
|
|
23
40
|
pathname,
|
|
24
41
|
}: GetRedirectArgs): Promise<GetRedirectReturn> {
|
|
@@ -29,7 +46,8 @@ export async function getRedirect({
|
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
try {
|
|
32
|
-
const redirectMatch =
|
|
49
|
+
const redirectMatch = resolveLocalMatch(pathname)
|
|
50
|
+
|
|
33
51
|
if (redirectMatch) {
|
|
34
52
|
return {
|
|
35
53
|
destination: encodeURI(redirectMatch.destination),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ServerProductQueryQuery } from '@generated/graphql'
|
|
2
|
+
|
|
3
|
+
type ServerOffers = ServerProductQueryQuery['product']['offers']
|
|
4
|
+
type ServerOffer = ServerOffers['offers'][number]
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schema.org properties the PDP JSON-LD is allowed to publish on an Offer.
|
|
8
|
+
*
|
|
9
|
+
* This is an allowlist on purpose. The PDP's server query and
|
|
10
|
+
* `ProductDetailsFragment_product` both select `offers.offers`, and GraphQL
|
|
11
|
+
* merges the two selection sets — so the offer object reaching this module
|
|
12
|
+
* carries UI-only fields (`priceWithTaxes`, `listPrice`, `listPriceWithTaxes`,
|
|
13
|
+
* `quantity`, `priceToken`) that have no meaning to Schema.org consumers.
|
|
14
|
+
* Excluding them by name would mean tracking a fragment this module does not
|
|
15
|
+
* own, which is exactly how they leaked into public markup in the first place.
|
|
16
|
+
*/
|
|
17
|
+
const SCHEMA_ORG_OFFER_FIELDS = [
|
|
18
|
+
'availability',
|
|
19
|
+
'itemCondition',
|
|
20
|
+
'price',
|
|
21
|
+
'priceValidUntil',
|
|
22
|
+
] as const satisfies ReadonlyArray<keyof ServerOffer>
|
|
23
|
+
|
|
24
|
+
// Emptiness, not falsiness: `price` is numeric and `0` is a real price, so a
|
|
25
|
+
// falsy check would strip it and leave an Offer with no price at all.
|
|
26
|
+
const isEmpty = (value: unknown) =>
|
|
27
|
+
value === null || value === undefined || value === ''
|
|
28
|
+
|
|
29
|
+
type ObjectLevelFields = {
|
|
30
|
+
priceCurrency: ServerOffers['priceCurrency']
|
|
31
|
+
url: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds the `offers` object for the PDP's `ProductJsonLd`, or `undefined` when
|
|
36
|
+
* the product has no offer to describe.
|
|
37
|
+
*
|
|
38
|
+
* `undefined` matters: an `Offer` carrying only a currency and a URL is invalid
|
|
39
|
+
* Schema.org, so omitting the property entirely beats emitting a priceless one.
|
|
40
|
+
* Every key is dropped when its value is empty — `priceValidUntil` and
|
|
41
|
+
* `availability` are both nullable upstream.
|
|
42
|
+
*/
|
|
43
|
+
export const toProductJsonLdOffer = (
|
|
44
|
+
serverOffer: ServerOffer | undefined,
|
|
45
|
+
{ priceCurrency, url }: ObjectLevelFields
|
|
46
|
+
) => {
|
|
47
|
+
if (!serverOffer) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const offer: Record<string, unknown> = { priceCurrency, url }
|
|
52
|
+
|
|
53
|
+
for (const field of SCHEMA_ORG_OFFER_FIELDS) {
|
|
54
|
+
offer[field] = serverOffer[field]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(offer)) {
|
|
58
|
+
if (isEmpty(value)) {
|
|
59
|
+
delete offer[key]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `price` resolves to null outside the search and order-form roots. Dropping
|
|
64
|
+
// the key alone would leave a priceless Offer — the same invalid markup the
|
|
65
|
+
// no-offer case above avoids, so it gets the same treatment.
|
|
66
|
+
if (isEmpty(offer.price)) {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return offer
|
|
71
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const STORE_ID = 'teststore'
|
|
4
|
+
const REWRITER_URL = `https://${STORE_ID}.myvtex.com/_v/public/redirect-evaluate`
|
|
5
|
+
|
|
6
|
+
const MATCHER_MODULE = 'src/customizations/src/redirects/index'
|
|
7
|
+
|
|
8
|
+
async function loadGetRedirect(matcherExport?: unknown) {
|
|
9
|
+
vi.resetModules()
|
|
10
|
+
|
|
11
|
+
vi.doMock('discovery.config', () => ({
|
|
12
|
+
default: { api: { storeId: STORE_ID } },
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
if (matcherExport === undefined) {
|
|
16
|
+
vi.doUnmock(MATCHER_MODULE)
|
|
17
|
+
} else {
|
|
18
|
+
vi.doMock(MATCHER_MODULE, () => ({ matcher: matcherExport }))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { getRedirect } = await import('../../../src/sdk/redirects')
|
|
22
|
+
|
|
23
|
+
return getRedirect
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mockRewriter({ ok, body }: { ok: boolean; body?: unknown }) {
|
|
27
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok, json: async () => body })
|
|
28
|
+
|
|
29
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
30
|
+
|
|
31
|
+
return fetchMock
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('getRedirect', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
37
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.doUnmock('discovery.config')
|
|
42
|
+
vi.doUnmock(MATCHER_MODULE)
|
|
43
|
+
vi.unstubAllGlobals()
|
|
44
|
+
vi.restoreAllMocks()
|
|
45
|
+
vi.resetModules()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('skips asset paths without querying the rewriter', async () => {
|
|
49
|
+
const fetchMock = mockRewriter({ ok: false })
|
|
50
|
+
const getRedirect = await loadGetRedirect()
|
|
51
|
+
|
|
52
|
+
await expect(getRedirect({ pathname: '/icons.json' })).resolves.toBeNull()
|
|
53
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('returns a custom matcher result without querying the rewriter', async () => {
|
|
57
|
+
const fetchMock = mockRewriter({ ok: false })
|
|
58
|
+
const getRedirect = await loadGetRedirect(() => ({
|
|
59
|
+
destination: '/promoção',
|
|
60
|
+
permanent: false,
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
await expect(getRedirect({ pathname: '/sale' })).resolves.toEqual({
|
|
64
|
+
destination: encodeURI('/promoção'),
|
|
65
|
+
permanent: false,
|
|
66
|
+
})
|
|
67
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
68
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('falls back to the rewriter when the matcher export is not a function', async () => {
|
|
72
|
+
const fetchMock = mockRewriter({
|
|
73
|
+
ok: true,
|
|
74
|
+
body: { location: '/eletrodomesticos', status: 308 },
|
|
75
|
+
})
|
|
76
|
+
// A store shipping `src/redirects.json` can shadow the customization
|
|
77
|
+
// module, which makes the imported `matcher` an array instead of a function.
|
|
78
|
+
const getRedirect = await loadGetRedirect([])
|
|
79
|
+
|
|
80
|
+
await expect(getRedirect({ pathname: '/old-page' })).resolves.toEqual({
|
|
81
|
+
destination: '/eletrodomesticos',
|
|
82
|
+
permanent: true,
|
|
83
|
+
})
|
|
84
|
+
expect(fetchMock).toHaveBeenCalledWith(`${REWRITER_URL}/old-page`)
|
|
85
|
+
expect(console.warn).toHaveBeenCalledTimes(1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('warns only once about an invalid matcher export', async () => {
|
|
89
|
+
mockRewriter({ ok: false })
|
|
90
|
+
const getRedirect = await loadGetRedirect([])
|
|
91
|
+
|
|
92
|
+
await getRedirect({ pathname: '/first' })
|
|
93
|
+
await getRedirect({ pathname: '/second' })
|
|
94
|
+
|
|
95
|
+
expect(console.warn).toHaveBeenCalledTimes(1)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not leak a mocked matcher into a later default load', async () => {
|
|
99
|
+
mockRewriter({ ok: false })
|
|
100
|
+
|
|
101
|
+
const withInvalidMatcher = await loadGetRedirect([])
|
|
102
|
+
await expect(withInvalidMatcher({ pathname: '/first' })).resolves.toBeNull()
|
|
103
|
+
|
|
104
|
+
const withStubMatcher = await loadGetRedirect()
|
|
105
|
+
await expect(withStubMatcher({ pathname: '/second' })).resolves.toBeNull()
|
|
106
|
+
|
|
107
|
+
expect(console.warn).toHaveBeenCalledTimes(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('marks non-permanent rewriter redirects as temporary', async () => {
|
|
111
|
+
mockRewriter({ ok: true, body: { location: '/new', status: 307 } })
|
|
112
|
+
const getRedirect = await loadGetRedirect()
|
|
113
|
+
|
|
114
|
+
await expect(getRedirect({ pathname: '/old' })).resolves.toEqual({
|
|
115
|
+
destination: '/new',
|
|
116
|
+
permanent: false,
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('returns null when the rewriter has no rule for the path', async () => {
|
|
121
|
+
mockRewriter({ ok: false })
|
|
122
|
+
const getRedirect = await loadGetRedirect()
|
|
123
|
+
|
|
124
|
+
await expect(getRedirect({ pathname: '/no-rule' })).resolves.toBeNull()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { toProductJsonLdOffer } from '../../src/utils/productJsonLd'
|
|
3
|
+
|
|
4
|
+
const OBJECT_LEVEL = { priceCurrency: 'USD', url: 'https://store.example/p' }
|
|
5
|
+
|
|
6
|
+
// Mirrors what actually reaches getStaticProps: the PDP query's Schema.org
|
|
7
|
+
// fields merged with the UI-only fields ProductDetailsFragment_product selects
|
|
8
|
+
// on the same `offers.offers` field.
|
|
9
|
+
const makeServerOffer = (overrides: Record<string, unknown> = {}) =>
|
|
10
|
+
({
|
|
11
|
+
availability: 'https://schema.org/InStock',
|
|
12
|
+
itemCondition: 'https://schema.org/NewCondition',
|
|
13
|
+
price: 12.5,
|
|
14
|
+
priceValidUntil: '2027-08-24T19:48:25Z',
|
|
15
|
+
// UI-only fields that must never reach the markup
|
|
16
|
+
listPrice: 20,
|
|
17
|
+
listPriceWithTaxes: 22,
|
|
18
|
+
priceWithTaxes: 13.75,
|
|
19
|
+
quantity: 10000,
|
|
20
|
+
priceToken: 'a-short-lived-pricing-token',
|
|
21
|
+
seller: { identifier: '1' },
|
|
22
|
+
...overrides,
|
|
23
|
+
}) as any
|
|
24
|
+
|
|
25
|
+
describe('toProductJsonLdOffer', () => {
|
|
26
|
+
it('emits exactly the allowed Schema.org keys', () => {
|
|
27
|
+
const offer = toProductJsonLdOffer(makeServerOffer(), OBJECT_LEVEL)
|
|
28
|
+
|
|
29
|
+
expect(Object.keys(offer!).sort()).toEqual([
|
|
30
|
+
'availability',
|
|
31
|
+
'itemCondition',
|
|
32
|
+
'price',
|
|
33
|
+
'priceCurrency',
|
|
34
|
+
'priceValidUntil',
|
|
35
|
+
'url',
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
'listPrice',
|
|
41
|
+
'listPriceWithTaxes',
|
|
42
|
+
'priceWithTaxes',
|
|
43
|
+
'quantity',
|
|
44
|
+
'priceToken',
|
|
45
|
+
'seller',
|
|
46
|
+
])('does not leak %s into the markup', (field) => {
|
|
47
|
+
const offer = toProductJsonLdOffer(makeServerOffer(), OBJECT_LEVEL)
|
|
48
|
+
|
|
49
|
+
expect(offer).not.toHaveProperty(field)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// The regression guarantee: the mapper picks by allowlist, so a field added
|
|
53
|
+
// to any fragment selecting offers.offers cannot reach public markup.
|
|
54
|
+
it('ignores a field a future fragment might add', () => {
|
|
55
|
+
const offer = toProductJsonLdOffer(
|
|
56
|
+
makeServerOffer({ someFutureInternalField: 'leaked' }),
|
|
57
|
+
OBJECT_LEVEL
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(offer).not.toHaveProperty('someFutureInternalField')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('carries the allowed values through unchanged', () => {
|
|
64
|
+
const offer = toProductJsonLdOffer(makeServerOffer(), OBJECT_LEVEL)
|
|
65
|
+
|
|
66
|
+
expect(offer).toEqual({
|
|
67
|
+
availability: 'https://schema.org/InStock',
|
|
68
|
+
itemCondition: 'https://schema.org/NewCondition',
|
|
69
|
+
price: 12.5,
|
|
70
|
+
priceValidUntil: '2027-08-24T19:48:25Z',
|
|
71
|
+
priceCurrency: 'USD',
|
|
72
|
+
url: 'https://store.example/p',
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it.each([
|
|
77
|
+
['priceValidUntil', ''],
|
|
78
|
+
['priceValidUntil', null],
|
|
79
|
+
['availability', null],
|
|
80
|
+
])('drops %s when it resolves to %p', (field, value) => {
|
|
81
|
+
const offer = toProductJsonLdOffer(
|
|
82
|
+
makeServerOffer({ [field]: value }),
|
|
83
|
+
OBJECT_LEVEL
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
expect(offer).not.toHaveProperty(field)
|
|
87
|
+
expect(offer).toHaveProperty('price')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Emptiness, not falsiness — a free item still has a price.
|
|
91
|
+
it('keeps a price of 0', () => {
|
|
92
|
+
const offer = toProductJsonLdOffer(
|
|
93
|
+
makeServerOffer({ price: 0 }),
|
|
94
|
+
OBJECT_LEVEL
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(offer).toHaveProperty('price', 0)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('drops an empty priceCurrency', () => {
|
|
101
|
+
const offer = toProductJsonLdOffer(makeServerOffer(), {
|
|
102
|
+
...OBJECT_LEVEL,
|
|
103
|
+
priceCurrency: '',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(offer).not.toHaveProperty('priceCurrency')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// An Offer with only a currency and a URL is invalid Schema.org, so the
|
|
110
|
+
// property is omitted entirely rather than emitted priceless.
|
|
111
|
+
it('returns undefined when the product has no offer', () => {
|
|
112
|
+
expect(toProductJsonLdOffer(undefined, OBJECT_LEVEL)).toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// StoreOffer.price resolves to null outside the search and order-form roots.
|
|
116
|
+
// Dropping the key alone would leave a priceless Offer, which is exactly the
|
|
117
|
+
// invalid markup the no-offer case avoids.
|
|
118
|
+
it.each([
|
|
119
|
+
['null', null],
|
|
120
|
+
['an empty string', ''],
|
|
121
|
+
])('returns undefined when the price is %s', (_label, price) => {
|
|
122
|
+
expect(
|
|
123
|
+
toProductJsonLdOffer(makeServerOffer({ price }), OBJECT_LEVEL)
|
|
124
|
+
).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ProductJsonLd } from 'next-seo'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { toProductJsonLdOffer } from '../../src/utils/productJsonLd'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pins the next-seo behaviour the PDP's JSON-LD depends on.
|
|
8
|
+
*
|
|
9
|
+
* `p.tsx` omits `mpn`, `gtin` and `releaseDate` by conditional spread rather
|
|
10
|
+
* than passing empty strings, and drops `seller` from the offer allowlist. Both
|
|
11
|
+
* choices only work because of how next-seo builds the script — it forwards
|
|
12
|
+
* unknown props untouched (so `""` would be published) and turns any truthy
|
|
13
|
+
* `seller` into an `Organization`, even when it has no name. A next-seo upgrade
|
|
14
|
+
* that changed either would silently reintroduce the defects from ticket
|
|
15
|
+
* #1449246, so the behaviour is asserted here instead of assumed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// `useAppDir` renders the script directly; without it next-seo emits through
|
|
19
|
+
// next/head, which produces no markup outside a Next document.
|
|
20
|
+
const renderJsonLd = (props: Record<string, unknown>) => {
|
|
21
|
+
const html = renderToStaticMarkup(
|
|
22
|
+
<ProductJsonLd useAppDir productName="Product" images={[]} {...props} />
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return JSON.parse(html.replace(/^[\s\S]*?>([\s\S]*)<\/script>[\s\S]*$/, '$1'))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('next-seo ProductJsonLd contract', () => {
|
|
29
|
+
it('publishes an empty string when a prop is passed empty', () => {
|
|
30
|
+
expect(renderJsonLd({ mpn: '' })).toHaveProperty('mpn', '')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('omits the key when the prop is absent — what the conditional spread relies on', () => {
|
|
34
|
+
expect(renderJsonLd({})).not.toHaveProperty('mpn')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('emits a nameless Organization for a seller that has no name', () => {
|
|
38
|
+
const jsonLd = renderJsonLd({
|
|
39
|
+
offers: { price: 1, seller: { identifier: '1' } },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(jsonLd.offers.seller).toEqual({ '@type': 'Organization' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('leaves the mapper output intact, adding only @type', () => {
|
|
46
|
+
const offers = toProductJsonLdOffer(
|
|
47
|
+
{
|
|
48
|
+
availability: 'https://schema.org/InStock',
|
|
49
|
+
itemCondition: 'https://schema.org/NewCondition',
|
|
50
|
+
price: 0.2,
|
|
51
|
+
priceValidUntil: '2027-08-24T19:48:25Z',
|
|
52
|
+
priceWithTaxes: 0.2,
|
|
53
|
+
listPrice: 0.2,
|
|
54
|
+
listPriceWithTaxes: 0.2,
|
|
55
|
+
quantity: 10000,
|
|
56
|
+
priceToken: 'a-short-lived-pricing-token',
|
|
57
|
+
seller: { identifier: '1' },
|
|
58
|
+
} as any,
|
|
59
|
+
{ priceCurrency: 'USD', url: 'https://store.example/p' }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const jsonLd = renderJsonLd({ offers })
|
|
63
|
+
|
|
64
|
+
expect(jsonLd.offers).toEqual({
|
|
65
|
+
'@type': 'Offer',
|
|
66
|
+
availability: 'https://schema.org/InStock',
|
|
67
|
+
itemCondition: 'https://schema.org/NewCondition',
|
|
68
|
+
price: 0.2,
|
|
69
|
+
priceCurrency: 'USD',
|
|
70
|
+
priceValidUntil: '2027-08-24T19:48:25Z',
|
|
71
|
+
url: 'https://store.example/p',
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|