@faststore/core 4.10.1-dev.2 → 4.10.1-dev.3

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,22 +1,45 @@
1
1
  import type { ContentEntry, EntryPathParams } from '@vtex/client-cp'
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
+ import { clientCMS, cmsPageCache } from '../../../src/server/cms'
4
5
  import { ContentService } from '../../../src/server/content/service'
5
- import type {
6
- ContentOptions,
7
- ContentParams,
6
+ import {
7
+ type ContentOptions,
8
+ type ContentParams,
9
+ ContentSourceType,
8
10
  } from '../../../src/server/content/types'
9
- import type * as ContentUtils from '../../../src/server/content/utils'
10
- import { isContentPlatformSource } from '../../../src/server/content/utils'
11
+ import { contentSource } from '../../../discovery.config'
12
+
13
+ type ContentSourceConfig = { type: string }
14
+ type DiscoveryConfigModule = {
15
+ default: Record<string, unknown>
16
+ contentSource: ContentSourceConfig
17
+ }
18
+
19
+ vi.mock('../../../discovery.config', async (importOriginal) => {
20
+ const actual = await importOriginal<DiscoveryConfigModule>()
21
+ const base = actual.default ?? actual
22
+ const mutableContentSource = {
23
+ ...(base.contentSource as ContentSourceConfig),
24
+ }
11
25
 
12
- vi.mock('../../../src/server/content/utils', async (importOriginal) => {
13
- const actual = await importOriginal<typeof ContentUtils>()
14
26
  return {
15
27
  ...actual,
16
- isContentPlatformSource: vi.fn(actual.isContentPlatformSource),
28
+ default: { ...base, contentSource: mutableContentSource },
29
+ contentSource: mutableContentSource,
17
30
  }
18
31
  })
19
32
 
33
+ const DEFAULT_CONTENT_SOURCE_TYPE = contentSource.type
34
+
35
+ const useContentPlatform = () => {
36
+ contentSource.type = ContentSourceType.ContentPlatform
37
+ }
38
+
39
+ const useHeadlessCms = () => {
40
+ contentSource.type = DEFAULT_CONTENT_SOURCE_TYPE
41
+ }
42
+
20
43
  type ContentServiceInternals = {
21
44
  createContentOptions: (params: ContentParams) => ContentOptions
22
45
  convertOptionsToParams: (options: ContentOptions) => EntryPathParams
@@ -175,12 +198,12 @@ describe('ContentService', () => {
175
198
  const FIVE_MINUTES = 5 * 60 * 1000
176
199
 
177
200
  beforeEach(() => {
178
- vi.mocked(isContentPlatformSource).mockReturnValue(true)
201
+ useContentPlatform()
179
202
  vi.spyOn(console, 'warn').mockImplementation(() => {})
180
203
  })
181
204
 
182
205
  afterEach(() => {
183
- vi.mocked(isContentPlatformSource).mockReset()
206
+ useHeadlessCms()
184
207
  vi.restoreAllMocks()
185
208
  vi.useRealTimers()
186
209
  })
@@ -373,4 +396,208 @@ describe('ContentService', () => {
373
396
  expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
374
397
  })
375
398
  })
399
+
400
+ describe('Content Platform single entry cache', () => {
401
+ const FIVE_MINUTES = 5 * 60 * 1000
402
+
403
+ beforeEach(() => {
404
+ useContentPlatform()
405
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
406
+ })
407
+
408
+ afterEach(() => {
409
+ useHeadlessCms()
410
+ vi.restoreAllMocks()
411
+ vi.useRealTimers()
412
+ })
413
+
414
+ it('shares one list and entry fetch across concurrent pages', async () => {
415
+ const { service, clientCP } = createServiceWithClient([['generic']])
416
+
417
+ const results = await Promise.all(
418
+ Array.from({ length: 20 }, () =>
419
+ service.getSingleContent({ contentType: 'globalSections' })
420
+ )
421
+ )
422
+
423
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
424
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
425
+ for (const result of results) {
426
+ expect(result.sections).toEqual([{ name: 'Generic' }])
427
+ }
428
+ })
429
+
430
+ it('keeps separate entries per content type and locale', async () => {
431
+ const { service, clientCP } = createServiceWithClient([['generic']])
432
+
433
+ await service.getSingleContent({ contentType: 'globalSections' })
434
+ await service.getSingleContent({ contentType: 'globalHeaderSections' })
435
+ await service.getSingleContent({
436
+ contentType: 'globalSections',
437
+ locale: 'en-US',
438
+ })
439
+ await service.getSingleContent({ contentType: 'globalSections' })
440
+
441
+ expect(
442
+ clientCP.listEntries.mock.calls.map(([params]) => [
443
+ params.contentType,
444
+ params.locale,
445
+ ])
446
+ ).toEqual([
447
+ ['globalSections', undefined],
448
+ ['globalHeaderSections', undefined],
449
+ ['globalSections', 'en-US'],
450
+ ])
451
+ })
452
+
453
+ it('caches an empty optional content type', async () => {
454
+ const { service, clientCP } = createServiceWithClient([[]])
455
+
456
+ const first = await service.getSingleContent({
457
+ contentType: 'globalFooterSections',
458
+ })
459
+ await service.getSingleContent({ contentType: 'globalFooterSections' })
460
+
461
+ expect(first).toEqual({})
462
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
463
+ expect(clientCP.getEntry).not.toHaveBeenCalled()
464
+ })
465
+
466
+ it('does not cache an empty required content type', async () => {
467
+ const { service, clientCP } = createServiceWithClient([[]])
468
+
469
+ const first = await service.getSingleContent({
470
+ contentType: 'globalSections',
471
+ })
472
+ await service.getSingleContent({ contentType: 'globalSections' })
473
+
474
+ expect(first).toEqual({})
475
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
476
+ })
477
+
478
+ it('picks up a required content type published after an empty lookup', async () => {
479
+ const { service, clientCP } = createServiceWithClient([['generic']])
480
+ clientCP.listEntries.mockResolvedValueOnce({ entries: [], scroll: null })
481
+
482
+ const before = await service.getSingleContent({
483
+ contentType: 'globalSections',
484
+ })
485
+ const after = await service.getSingleContent({
486
+ contentType: 'globalSections',
487
+ })
488
+ await service.getSingleContent({ contentType: 'globalSections' })
489
+
490
+ expect(before).toEqual({})
491
+ expect(after.sections).toEqual([{ name: 'Generic' }])
492
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
493
+ })
494
+
495
+ it('does not cache lookups by slug', async () => {
496
+ const { service, clientCP } = createServiceWithClient([['generic']])
497
+
498
+ await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
499
+ await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
500
+
501
+ expect(clientCP.getEntryBySlug).toHaveBeenCalledTimes(2)
502
+ expect(clientCP.listEntries).not.toHaveBeenCalled()
503
+ })
504
+
505
+ it('bypasses the cache for branch previews', async () => {
506
+ const { service, clientCP } = createServiceWithClient([['generic']])
507
+ const branchPreview: ContentParams = {
508
+ contentType: 'globalSections',
509
+ previewData: {
510
+ contentType: 'landingPage',
511
+ documentId: 'some-landing-page',
512
+ versionId: 'branch-1',
513
+ },
514
+ }
515
+
516
+ await service.getSingleContent({ contentType: 'globalSections' })
517
+ await service.getSingleContent(branchPreview)
518
+ await service.getSingleContent(branchPreview)
519
+ await service.getSingleContent({ contentType: 'globalSections' })
520
+
521
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
522
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
523
+ expect(clientCP.previewEntryById).toHaveBeenCalledTimes(2)
524
+ })
525
+
526
+ it('does not cache a multiple-entries error', async () => {
527
+ const { service, clientCP } = createServiceWithClient([
528
+ ['generic', 'collection'],
529
+ ])
530
+
531
+ await expect(
532
+ service.getSingleContent({ contentType: 'globalSections' })
533
+ ).rejects.toThrow()
534
+ await expect(
535
+ service.getSingleContent({ contentType: 'globalSections' })
536
+ ).rejects.toThrow()
537
+
538
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
539
+ })
540
+
541
+ it('fetches again after the TTL', async () => {
542
+ vi.useFakeTimers()
543
+ const { service, clientCP } = createServiceWithClient([['generic']])
544
+
545
+ await service.getSingleContent({ contentType: 'globalSections' })
546
+ vi.advanceTimersByTime(FIVE_MINUTES)
547
+ await service.getSingleContent({ contentType: 'globalSections' })
548
+
549
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
550
+ })
551
+ })
552
+
553
+ describe('Headless CMS single content cache', () => {
554
+ const locator = {
555
+ contentType: 'globalSections',
556
+ documentId: 'doc-1',
557
+ versionId: 'version-1',
558
+ }
559
+
560
+ beforeEach(() => {
561
+ useHeadlessCms()
562
+ cmsPageCache.clear()
563
+ vi.spyOn(clientCMS, 'getCMSPage').mockResolvedValue({
564
+ id: 'doc-1',
565
+ name: 'doc-1',
566
+ status: 'published',
567
+ type: 'globalSections',
568
+ releaseId: 'release-1',
569
+ sections: [{ name: 'Children' }],
570
+ } as Awaited<ReturnType<typeof clientCMS.getCMSPage>>)
571
+ })
572
+
573
+ afterEach(() => {
574
+ useHeadlessCms()
575
+ vi.restoreAllMocks()
576
+ })
577
+
578
+ it('reuses a published document fetched by locator', async () => {
579
+ const service = new ContentService()
580
+
581
+ await Promise.all([
582
+ service.getSingleContent(locator),
583
+ service.getSingleContent(locator),
584
+ ])
585
+ await service.getSingleContent(locator)
586
+
587
+ expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(1)
588
+ })
589
+
590
+ it('fetches the document on every preview request', async () => {
591
+ const service = new ContentService()
592
+ const previewParams: ContentParams = {
593
+ ...locator,
594
+ previewData: locator,
595
+ }
596
+
597
+ await service.getSingleContent(previewParams)
598
+ await service.getSingleContent(previewParams)
599
+
600
+ expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(2)
601
+ })
602
+ })
376
603
  })