@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.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.4 generate /home/runner/work/faststore/faststore/packages/core
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.4 gen-types /home/runner/work/faststore/faststore/packages/core
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
  [STARTED] Parse Configuration
@@ -19,7 +19,7 @@
19
19
  [COMPLETED] Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
20
20
  [COMPLETED] Generate outputs
21
21
 
22
- > @faststore/core@4.7.0-dev.4 cache-graphql /home/runner/work/faststore/faststore/packages/core
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
@@ -1,5 +1,5 @@
1
1
 
2
- > @faststore/core@4.7.0-dev.4 test /home/runner/work/faststore/faststore/packages/core
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
- ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1325ms
10
+ ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 1180ms
11
11
  Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
12
12
  npx update-browserslist-db@latest
13
13
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
14
- ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 260ms
15
- ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2965ms
16
- ✓ shows an error state when the list fails to load  863ms
17
- ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  427ms
18
- ✓ keeps Confirm disabled when switching is unavailable  372ms
19
14
  ✓  browser  test/components/account/cards/MyAccountListCards.browser.test.tsx (14 tests) 2735ms
20
- ✓ shows the tab bar and renders Personal cards by default  1165ms
21
- ✓ paginates when there are more than 25 cards  380ms
22
- ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 30ms
23
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 218ms
15
+ ✓ shows the tab bar and renders Personal cards by default  986ms
16
+ ✓ paginates when there are more than 25 cards  484ms
17
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2741ms
18
+ ✓ shows an error state when the list fails to load  834ms
19
+ ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  480ms
20
+ ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 286ms
21
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 28ms
22
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (12 tests) 128ms
24
23
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
25
24
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
26
25
   at node:internal/deps/undici/undici:16416:13
@@ -50,7 +49,7 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
50
49
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
51
50
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
52
51
 
53
- ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 292ms
52
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (7 tests) 234ms
54
53
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > posts the password and redirects to the requested return path
55
54
  Warning: 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
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 32ms
139
139
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > uses the fallback invalid-password message when API omits an error
140
140
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
141
141
 
@@ -184,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
- ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 31ms
187
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1658ms
188
+ ✓ renders the menu view with contract details and manage footer  988ms
188
189
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > shows service unavailable when the response is not a login payload
189
190
  Warning: 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
- ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1454ms
285
- ✓ renders the password protection form  616ms
286
- ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1396ms
287
- ✓ renders the menu view with contract details and manage footer  889ms
288
- ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 24ms
289
- stderr | test/components/cms/RenderSections.browser.test.tsx
285
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1773ms
286
+ ✓ renders the password protection form  871ms
287
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 33ms
288
+ stderr | test/sdk/localization/store-url.browser.test.ts
290
289
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
291
290
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
292
291
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:120:9)
@@ -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
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
296
295
 
297
- stderr | test/sdk/localization/store-url.browser.test.ts
296
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 6ms
297
+ stderr | test/components/cms/RenderSections.browser.test.tsx
298
298
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
299
299
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
300
300
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:120:9)
@@ -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
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
304
304
 
305
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 6ms
306
- ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 81ms
305
+ ✓  browser  test/components/cms/RenderSections.browser.test.tsx (5 tests) 91ms
307
306
  ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 34ms
308
- ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 35ms
309
- ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 42ms
310
- ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 137ms
307
+ ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 43ms
308
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.sessionFallback.browser.test.tsx (2 tests) 129ms
309
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 43ms
311
310
  stderr | test/components/account/doLogout.browser.test.ts
312
311
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
313
312
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -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
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
318
317
 
319
- ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 20ms
320
- ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 86ms
321
- ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 317ms
318
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 29ms
319
+ ✓  browser  test/components/sections/Account/AccountListCards.browser.test.tsx (3 tests) 98ms
320
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 328ms
321
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 313ms
322
322
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
323
323
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
324
324
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -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
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
329
329
 
330
- ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 13ms
331
- ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 6ms
332
- ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 322ms
333
- ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 4ms
330
+ ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 7ms
331
+ ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 5ms
332
+ ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 3ms
333
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 32ms
334
334
  stderr | test/sdk/account/refreshToken.browser.test.ts
335
335
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
336
336
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -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
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
341
341
 
342
- ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 5ms
343
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 21ms
344
- ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 40ms
345
- ✓  node  test/utils/match-url.test.ts (10 tests) 12ms
346
342
  ✓  node  test/server/password-protection-service.test.ts (22 tests) 38ms
347
- ✓  node  test/pages/api/unlock.test.ts (15 tests) 260ms
348
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 21ms
349
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 10ms
343
+ ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 6ms
344
+ ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 29ms
345
+ ✓  node  test/utils/match-url.test.ts (10 tests) 9ms
346
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 229ms
347
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 16ms
348
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 9ms
350
349
  stderr | test/components/search/SearchInputComponent.test.tsx
351
350
  Warning: 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
- ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 73ms
414
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 53ms
415
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 17ms
412
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 82ms
413
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 58ms
414
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 15ms
416
415
  stderr | test/pages/pvt/account/cards/index.test.ts > Cards page getServerSideProps > redirects to /pvt/account/403 when the query fails with 401/403
417
416
  { extensions: { status: 401 } }
418
417
 
419
418
  stderr | test/pages/pvt/account/cards/index.test.ts > Cards page getServerSideProps > renders the error state (no redirect) when the query fails with a non-auth status
420
419
  { extensions: { status: 502 } }
421
420
 
422
- ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 14ms
423
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 28ms
424
- ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 11ms
425
- ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 441ms
421
+ ✓  node  test/pages/pvt/account/cards/index.test.ts (8 tests) 12ms
422
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 22ms
423
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 10ms
424
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 451ms
426
425
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
427
426
  Graphql 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/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37
516
515
  at Traces.$ (file:///home/runner/work/faststore/faststore/node_modules/.pnpm/vitest@4.0.18_@opentelemetry+api@1.9.0_@types+node@20.19.13_jiti@2.6.1_jsdom@27.4.0_sas_13be941c57d55f08f06eeaf1c180ddfe/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27)
517
516
 
518
- ✓  node  test/pages/api/graphql.test.ts (17 tests) 24ms
519
- ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 851ms
520
- ✓ navigates with the search term on blur  362ms
521
- ✓ navigates without a label param when the search is cleared  361ms
517
+ ✓  node  test/pages/api/graphql.test.ts (17 tests) 32ms
518
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 811ms
519
+ ✓ navigates with the search term on blur  361ms
520
+ ✓ navigates without a label param when the search is cleared  359ms
522
521
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
523
522
  Set password request failed: Bad Request
524
523
 
525
- ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 46ms
526
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 8ms
527
- ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 37ms
528
- ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 39ms
524
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 34ms
525
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 43ms
526
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 41ms
527
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 7ms
529
528
  ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 7ms
530
- ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 9ms
531
- ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 65ms
532
- ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 169ms
533
- ✓  node  test/proxy.localization.test.ts (5 tests) 53ms
534
- ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 44ms
535
- ✓  node  test/server/index.test.ts (7 tests) 945ms
536
- ✓ should handle options and execute  383ms
529
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (12 tests) 10ms
530
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 45ms
531
+ ✓  node  test/sdk/redirects/index.test.ts (7 tests) 41ms
532
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 182ms
533
+ ✓  node  test/utils/productJsonLd.test.ts (17 tests) 10ms
534
+ ✓  node  test/proxy.localization.test.ts (5 tests) 56ms
535
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 46ms
536
+ ✓  node  test/server/index.test.ts (7 tests) 1302ms
537
+ ✓ should return a valid merged GraphQL schema  334ms
538
+ ✓ should handle options and execute  689ms
537
539
  ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 7ms
538
- ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 113ms
540
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 118ms
539
541
  ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 48ms
540
- ✓  node  test/proxy.test.ts (4 tests) 18ms
541
- ✓  node  test/server/webops-api.test.ts (9 tests) 24ms
542
- ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 39ms
543
- ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 15ms
544
- ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 7ms
545
- ✓  node  test/server/content/service.test.ts (5 tests) 6ms
546
- ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 9ms
547
- ✓  node  test/server/cms/global.test.ts (3 tests) 6ms
548
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 4ms
549
- ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 34ms
550
- ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 147ms
542
+ ✓  node  test/server/webops-api.test.ts (9 tests) 16ms
543
+ ✓  node  test/proxy.test.ts (4 tests) 17ms
544
+ ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 12ms
545
+ ✓  node  test/server/cms/myAccountDefaultSections.test.ts (7 tests) 8ms
546
+ ✓  node  test/server/content/service.test.ts (5 tests) 8ms
547
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 45ms
548
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 8ms
549
+ ✓  node  test/utils/productJsonLdRendering.test.tsx (4 tests) 16ms
550
+ ✓  node  test/server/cms/global.test.ts (3 tests) 7ms
551
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 5ms
552
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 31ms
553
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 163ms
551
554
  ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 7ms
552
- ✓  node  test/pages/api/preview.test.ts (2 tests) 7ms
553
- ✓  node  test/server/rootSpan.test.ts (4 tests) 44ms
554
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 5ms
555
- ✓  node  test/components/account/Layout.test.tsx (6 tests) 69ms
556
- ✓  node  test/components/search/SearchInput.test.ts (6 tests) 7ms
557
- ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 5ms
558
- ✓  node  test/utils/retry.test.ts (5 tests) 14ms
555
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 10ms
556
+ ✓  node  test/server/rootSpan.test.ts (4 tests) 49ms
557
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 11ms
558
+ ✓  node  test/components/search/SearchInput.test.ts (6 tests) 6ms
559
+ ✓  node  test/components/account/Layout.test.tsx (6 tests) 76ms
560
+ ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 7ms
561
+ ✓  node  test/utils/retry.test.ts (5 tests) 15ms
559
562
  ✓  node  test/server/cms/index.test.ts (2 tests) 6ms
560
- ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 tests) 6ms
561
- ✓  node  test/utils/unlockResponse.test.ts (11 tests) 6ms
563
+ ✓  node  test/sdk/account/getB2BSessionClaims.test.ts (4 tests) 5ms
564
+ ✓  node  test/utils/unlockResponse.test.ts (11 tests) 5ms
562
565
  ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 6ms
563
- ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 184ms
566
+ ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 192ms
567
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 52ms
564
568
  ✓  node  test/utils/isLocalHost.test.ts (7 tests) 6ms
565
- ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 57ms
566
- ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 4ms
569
+ ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 5ms
567
570
  ✓  node  test/utils/getCookie.test.ts (4 tests) 5ms
571
+ ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 44ms
568
572
  ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 5ms
569
- ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 30ms
570
- ✓  node  test/instrumentation.test.ts (3 tests) 6ms
571
- ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 8ms
572
- ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 25ms
573
-
574
-  Test Files  88 passed (88)
575
-  Tests  702 passed (702)
576
-  Start at  21:33:38
577
-  Duration  45.95s (transform 12.76s, setup 0ms, import 41.15s, tests 16.16s, environment 54.47s)
573
+ ✓  node  test/instrumentation.test.ts (3 tests) 7ms
574
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 28ms
575
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 16ms
576
+
577
+  Test Files  91 passed (91)
578
+  Tests  730 passed (730)
579
+  Start at  17:35:39
580
+  Duration  45.76s (transform 13.02s, setup 0ms, import 41.25s, tests 16.71s, environment 55.14s)
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.5",
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.5",
75
- "@faststore/diagnostics": "4.7.0-dev.5",
76
- "@faststore/lighthouse": "4.7.0-dev.5",
77
- "@faststore/sdk": "4.7.0-dev.5",
78
- "@faststore/ui": "4.7.0-dev.5"
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",
@@ -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
- gtin={product.gtin}
332
- mpn={product.mpn}
333
- releaseDate={product.releaseDate}
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={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
- let offer = {}
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 = matcher({ pathname })
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
+ })