kmcom-nuxt-layers 6.4.0 → 6.5.0

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.
Files changed (129) hide show
  1. package/docs/MIGRATION-v6.5.0.md +87 -0
  2. package/docs/MIGRATION.md +2 -1
  3. package/docs/USAGE.md +35 -10
  4. package/layers/content/app/components/content/Figure.vue +1 -1
  5. package/layers/content/app/composables/useContentSection.ts +1 -1
  6. package/layers/core/app/app.config.ts +7 -3
  7. package/layers/core/app/components/LoadingScreen.vue +35 -21
  8. package/layers/core/app/composables/useCache.ts +2 -2
  9. package/layers/core/app/composables/useErrorLog.ts +5 -5
  10. package/layers/core/app/composables/useLoading.ts +19 -10
  11. package/layers/core/app/plugins/loading.client.ts +11 -3
  12. package/layers/core/app/types/loading.ts +10 -3
  13. package/layers/core/app/utils/loading.ts +6 -15
  14. package/layers/core/nuxt.config.ts +14 -14
  15. package/layers/core/server/utils/app-config.ts +17 -0
  16. package/layers/data/auth/app/middleware/auth.ts +1 -1
  17. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  18. package/layers/data/forms/server/api/contact.post.ts +16 -8
  19. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  20. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  21. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  22. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  23. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  24. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  25. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  26. package/layers/delivery/feeds/package.json +2 -2
  27. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  28. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  29. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  30. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  31. package/layers/design-system/theming/nuxt.config.ts +0 -2
  32. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  33. package/layers/render/canvas/nuxt.config.ts +0 -4
  34. package/layers/render/shader/nuxt.config.ts +0 -4
  35. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  36. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  37. package/layers/structure/routing/nuxt.config.ts +19 -12
  38. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  39. package/package.json +35 -5
  40. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  41. package/docs/IMPROVE-AUDIT-README.md +0 -29
  42. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  43. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  44. package/docs/fallow-refactor/apps-debug.md +0 -27
  45. package/docs/fallow-refactor/apps-playground.md +0 -46
  46. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  47. package/docs/fallow-refactor/layers-animations.md +0 -34
  48. package/docs/fallow-refactor/layers-canvas.md +0 -32
  49. package/docs/fallow-refactor/layers-content.md +0 -33
  50. package/docs/fallow-refactor/layers-core.md +0 -39
  51. package/docs/fallow-refactor/layers-feeds.md +0 -39
  52. package/docs/fallow-refactor/layers-forms.md +0 -30
  53. package/docs/fallow-refactor/layers-layout.md +0 -42
  54. package/docs/fallow-refactor/layers-mailer.md +0 -32
  55. package/docs/fallow-refactor/layers-motion.md +0 -27
  56. package/docs/fallow-refactor/layers-navigation.md +0 -31
  57. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  58. package/docs/fallow-refactor/layers-routing.md +0 -33
  59. package/docs/fallow-refactor/layers-scripts.md +0 -35
  60. package/docs/fallow-refactor/layers-scroll.md +0 -38
  61. package/docs/fallow-refactor/layers-seo.md +0 -32
  62. package/docs/fallow-refactor/layers-shader.md +0 -53
  63. package/docs/fallow-refactor/layers-theme.md +0 -33
  64. package/docs/fallow-refactor/layers-transitions.md +0 -27
  65. package/docs/fallow-refactor/layers-typography.md +0 -29
  66. package/docs/fallow-refactor/layers-visual.md +0 -34
  67. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  68. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  69. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  70. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  71. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  72. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  73. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  74. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  75. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  76. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  77. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  78. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  79. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  80. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  81. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  82. package/layers/core/app/components/Element.test.ts +0 -90
  83. package/layers/core/app/components/ElementTw.test.ts +0 -64
  84. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  85. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  86. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  87. package/layers/core/app/utils/loading.test.ts +0 -48
  88. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  89. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  90. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  91. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  92. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  93. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  94. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  95. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  96. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  97. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  98. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  99. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  100. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  101. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  102. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  103. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  104. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  105. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  106. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  107. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  108. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  109. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  110. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  111. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  112. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  113. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  114. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  115. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  116. package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +0 -38
  117. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  118. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  119. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  120. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  121. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  122. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  123. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  124. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  125. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  126. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  127. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  128. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  129. package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
@@ -1,144 +0,0 @@
1
- import { describe, expect, it, vi } from 'vite-plus/test'
2
-
3
- import { openLibraryCoverUrl } from './client'
4
- import {
5
- normaliseOpenLibraryEdition,
6
- normaliseOpenLibrarySearchDoc,
7
- normaliseOpenLibraryWork,
8
- } from './normalise'
9
- import type { OpenLibraryEdition, OpenLibrarySearchDoc, OpenLibraryWork } from './types'
10
-
11
- // normalise.ts calls openLibraryCoverUrl() as a Nitro auto-import; stub it with the real
12
- // implementation from client.ts since that function is pure (no $fetch/config involved).
13
- vi.stubGlobal('openLibraryCoverUrl', openLibraryCoverUrl)
14
-
15
- function baseSearchDoc(overrides: Partial<OpenLibrarySearchDoc> = {}): OpenLibrarySearchDoc {
16
- return {
17
- key: '/works/OL82563W',
18
- title: 'Watchmen',
19
- ...overrides,
20
- }
21
- }
22
-
23
- function baseWork(overrides: Partial<OpenLibraryWork> = {}): OpenLibraryWork {
24
- return {
25
- key: '/works/OL82563W',
26
- title: 'Watchmen',
27
- ...overrides,
28
- }
29
- }
30
-
31
- function baseEdition(overrides: Partial<OpenLibraryEdition> = {}): OpenLibraryEdition {
32
- return {
33
- key: '/books/OL456M',
34
- title: 'Watchmen',
35
- ...overrides,
36
- }
37
- }
38
-
39
- describe('normaliseOpenLibrarySearchDoc', () => {
40
- it('strips /works/ from the key to derive the id', () => {
41
- const record = normaliseOpenLibrarySearchDoc(baseSearchDoc({ key: '/works/OL82563W' }))
42
- expect(record.providerId).toBe('OL82563W')
43
- expect(record.id).toBe('openlibrary:work:OL82563W')
44
- })
45
-
46
- it('splits isbn[] into isbn10/isbn13 by length', () => {
47
- const record = normaliseOpenLibrarySearchDoc(
48
- baseSearchDoc({ isbn: ['0930289232', '9780930289232'] })
49
- )
50
- expect(record.identifiers).toMatchObject({ isbn10: '0930289232', isbn13: '9780930289232' })
51
- })
52
-
53
- it('leaves isbn10/isbn13 undefined when isbn is absent', () => {
54
- const record = normaliseOpenLibrarySearchDoc(baseSearchDoc())
55
- expect(record.identifiers?.isbn10).toBeUndefined()
56
- expect(record.identifiers?.isbn13).toBeUndefined()
57
- })
58
-
59
- it('maps author_name/author_key pairs into creators, stripping /authors/', () => {
60
- const record = normaliseOpenLibrarySearchDoc(
61
- baseSearchDoc({ author_name: ['Alan Moore'], author_key: ['/authors/OL123A'] })
62
- )
63
- expect(record.creators).toEqual([{ name: 'Alan Moore', role: 'author', providerId: 'OL123A' }])
64
- })
65
-
66
- it('leaves creators undefined when there are no authors', () => {
67
- expect(normaliseOpenLibrarySearchDoc(baseSearchDoc()).creators).toBeUndefined()
68
- })
69
-
70
- it('builds coverUrl from cover_i using openLibraryCoverUrl', () => {
71
- const record = normaliseOpenLibrarySearchDoc(baseSearchDoc({ cover_i: 12345 }))
72
- expect(record.coverUrl).toBe(openLibraryCoverUrl(12345))
73
- })
74
-
75
- it('leaves coverUrl undefined when cover_i is absent', () => {
76
- expect(normaliseOpenLibrarySearchDoc(baseSearchDoc()).coverUrl).toBeUndefined()
77
- })
78
-
79
- it('stamps lastSyncedAt as a string', () => {
80
- expect(normaliseOpenLibrarySearchDoc(baseSearchDoc()).lastSyncedAt).toEqual(expect.any(String))
81
- })
82
- })
83
-
84
- describe('normaliseOpenLibraryWork', () => {
85
- it('strips /works/ from the key to derive the id', () => {
86
- const record = normaliseOpenLibraryWork(baseWork({ key: '/works/OL82563W' }))
87
- expect(record.providerId).toBe('OL82563W')
88
- expect(record.id).toBe('openlibrary:work:OL82563W')
89
- })
90
-
91
- it('uses the description string directly when it is a string', () => {
92
- const record = normaliseOpenLibraryWork(baseWork({ description: 'Plain text description' }))
93
- expect(record.description).toBe('Plain text description')
94
- })
95
-
96
- it('extracts description.value when description is an object', () => {
97
- const record = normaliseOpenLibraryWork(
98
- baseWork({ description: { value: 'Wrapped description' } })
99
- )
100
- expect(record.description).toBe('Wrapped description')
101
- })
102
-
103
- it('leaves description undefined when absent', () => {
104
- expect(normaliseOpenLibraryWork(baseWork()).description).toBeUndefined()
105
- })
106
-
107
- it('builds coverUrl from covers[0]', () => {
108
- const record = normaliseOpenLibraryWork(baseWork({ covers: [777, 888] }))
109
- expect(record.coverUrl).toBe(openLibraryCoverUrl(777))
110
- })
111
- })
112
-
113
- describe('normaliseOpenLibraryEdition', () => {
114
- it('strips /books/ from the key to derive the id', () => {
115
- const record = normaliseOpenLibraryEdition(baseEdition({ key: '/books/OL456M' }))
116
- expect(record.providerId).toBe('OL456M')
117
- expect(record.id).toBe('openlibrary:edition:OL456M')
118
- })
119
-
120
- it('reads isbn10/isbn13 from the first entries of isbn_10/isbn_13', () => {
121
- const record = normaliseOpenLibraryEdition(
122
- baseEdition({ isbn_10: ['0930289232'], isbn_13: ['9780930289232', '9999999999999'] })
123
- )
124
- expect(record.identifiers).toMatchObject({ isbn10: '0930289232', isbn13: '9780930289232' })
125
- })
126
-
127
- it('leaves isbn10/isbn13 undefined when absent', () => {
128
- const record = normaliseOpenLibraryEdition(baseEdition())
129
- expect(record.identifiers?.isbn10).toBeUndefined()
130
- expect(record.identifiers?.isbn13).toBeUndefined()
131
- })
132
-
133
- it('builds coverUrl from covers[0]', () => {
134
- const record = normaliseOpenLibraryEdition(baseEdition({ covers: [111] }))
135
- expect(record.coverUrl).toBe(openLibraryCoverUrl(111))
136
- })
137
-
138
- it('takes publisher from publishers[0]', () => {
139
- const record = normaliseOpenLibraryEdition(
140
- baseEdition({ publishers: ['DC Comics', 'Vertigo'] })
141
- )
142
- expect(record.publisher).toBe('DC Comics')
143
- })
144
- })
@@ -1,29 +0,0 @@
1
- import { describe, expect, it, vi } from 'vite-plus/test'
2
-
3
- vi.stubGlobal('useRuntimeConfig', () => ({
4
- metadataTmdb: {
5
- apiKey: 'test-key',
6
- baseUrl: 'https://api.themoviedb.org/3',
7
- imageBaseUrl: 'https://image.tmdb.org/t/p',
8
- },
9
- }))
10
-
11
- const { tmdbImageUrl } = await import('./client')
12
-
13
- describe('tmdbImageUrl', () => {
14
- it('returns undefined when path is null', () => {
15
- expect(tmdbImageUrl(null)).toBeUndefined()
16
- })
17
-
18
- it('defaults size to w500', () => {
19
- expect(tmdbImageUrl('/poster.jpg')).toBe('https://image.tmdb.org/t/p/w500/poster.jpg')
20
- })
21
-
22
- it('uses a custom size when provided', () => {
23
- expect(tmdbImageUrl('/poster.jpg', 'w200')).toBe('https://image.tmdb.org/t/p/w200/poster.jpg')
24
- })
25
-
26
- it('prefixes imageBaseUrl from runtime config', () => {
27
- expect(tmdbImageUrl('/backdrop.jpg')).toBe('https://image.tmdb.org/t/p/w500/backdrop.jpg')
28
- })
29
- })
@@ -1,162 +0,0 @@
1
- import { describe, expect, it, vi } from 'vite-plus/test'
2
-
3
- import { tmdbImageUrl } from './client'
4
- import { normaliseTmdbMovie, normaliseTmdbSearchResult, normaliseTmdbTV } from './normalise'
5
- import type { TMDBMovie, TMDBSearchMultiResult, TMDBTVShow } from './types'
6
-
7
- // normalise.ts calls tmdbImageUrl() and getConfig() -> useRuntimeConfig() as Nitro auto-imports;
8
- // stub the runtime config and delegate to the real (pure, string-building) client implementation.
9
- vi.stubGlobal('useRuntimeConfig', () => ({
10
- metadataTmdb: {
11
- apiKey: 'test-key',
12
- baseUrl: 'https://api.themoviedb.org/3',
13
- imageBaseUrl: 'https://image.tmdb.org/t/p',
14
- },
15
- }))
16
- vi.stubGlobal('tmdbImageUrl', tmdbImageUrl)
17
-
18
- function baseMovie(overrides: Partial<TMDBMovie> = {}): TMDBMovie {
19
- return {
20
- id: 550,
21
- title: 'Fight Club',
22
- overview: '',
23
- poster_path: null,
24
- backdrop_path: null,
25
- release_date: '',
26
- ...overrides,
27
- }
28
- }
29
-
30
- function baseTV(overrides: Partial<TMDBTVShow> = {}): TMDBTVShow {
31
- return {
32
- id: 1399,
33
- name: 'Game of Thrones',
34
- overview: '',
35
- poster_path: null,
36
- backdrop_path: null,
37
- first_air_date: '',
38
- ...overrides,
39
- }
40
- }
41
-
42
- function baseSearchResult(overrides: Partial<TMDBSearchMultiResult> = {}): TMDBSearchMultiResult {
43
- return {
44
- id: 1,
45
- media_type: 'movie',
46
- poster_path: null,
47
- ...overrides,
48
- }
49
- }
50
-
51
- describe('normaliseTmdbMovie', () => {
52
- it('filters directors from credits.crew by job === "Director"', () => {
53
- const record = normaliseTmdbMovie(
54
- baseMovie({
55
- credits: {
56
- cast: [],
57
- crew: [
58
- { id: 1, name: 'David Fincher', job: 'Director', department: 'Directing' },
59
- { id: 2, name: 'Jim Uhls', job: 'Screenplay', department: 'Writing' },
60
- ],
61
- },
62
- })
63
- )
64
- expect(record.creators).toEqual([{ name: 'David Fincher', role: 'Director', providerId: '1' }])
65
- })
66
-
67
- it('omits creators entirely when there are no directors', () => {
68
- const record = normaliseTmdbMovie(baseMovie())
69
- expect('creators' in record).toBe(false)
70
- })
71
-
72
- it('omits description, publisher, publishedAt and coverUrl when source fields are absent', () => {
73
- const record = normaliseTmdbMovie(baseMovie())
74
- expect('description' in record).toBe(false)
75
- expect('publisher' in record).toBe(false)
76
- expect('publishedAt' in record).toBe(false)
77
- expect('coverUrl' in record).toBe(false)
78
- })
79
-
80
- it('includes description, publisher, publishedAt and coverUrl when source fields are present', () => {
81
- const record = normaliseTmdbMovie(
82
- baseMovie({
83
- overview: 'A depressed man forms an underground fight club.',
84
- production_companies: [{ id: 1, name: 'Fox 2000 Pictures' }],
85
- release_date: '1999-10-15',
86
- poster_path: '/poster.jpg',
87
- })
88
- )
89
- expect(record.description).toBe('A depressed man forms an underground fight club.')
90
- expect(record.publisher).toBe('Fox 2000 Pictures')
91
- expect(record.publishedAt).toBe('1999-10-15')
92
- expect(record.coverUrl).toBe('https://image.tmdb.org/t/p/w500/poster.jpg')
93
- })
94
-
95
- it('includes imdbId in identifiers only when external_ids.imdb_id is present', () => {
96
- const withImdb = normaliseTmdbMovie(baseMovie({ external_ids: { imdb_id: 'tt0137523' } }))
97
- expect(withImdb.identifiers).toEqual({ tmdbId: '550', imdbId: 'tt0137523' })
98
-
99
- const withoutImdb = normaliseTmdbMovie(baseMovie())
100
- expect(withoutImdb.identifiers).toEqual({ tmdbId: '550' })
101
- expect('imdbId' in (withoutImdb.identifiers as object)).toBe(false)
102
- })
103
-
104
- it('stamps lastSyncedAt as a string', () => {
105
- expect(normaliseTmdbMovie(baseMovie()).lastSyncedAt).toEqual(expect.any(String))
106
- })
107
- })
108
-
109
- describe('normaliseTmdbTV', () => {
110
- it('maps created_by to Creator-role creators', () => {
111
- const record = normaliseTmdbTV(baseTV({ created_by: [{ id: 9, name: 'David Benioff' }] }))
112
- expect(record.creators).toEqual([{ name: 'David Benioff', role: 'Creator', providerId: '9' }])
113
- })
114
-
115
- it('omits creators when created_by is absent', () => {
116
- const record = normaliseTmdbTV(baseTV())
117
- expect('creators' in record).toBe(false)
118
- })
119
-
120
- it('takes publisher from the first network', () => {
121
- const record = normaliseTmdbTV(baseTV({ networks: [{ id: 1, name: 'HBO' }] }))
122
- expect(record.publisher).toBe('HBO')
123
- })
124
-
125
- it('includes imdbId in identifiers only when external_ids.imdb_id is present', () => {
126
- const record = normaliseTmdbTV(baseTV({ external_ids: { imdb_id: 'tt0944947' } }))
127
- expect(record.identifiers).toEqual({ tmdbId: '1399', imdbId: 'tt0944947' })
128
- })
129
- })
130
-
131
- describe('normaliseTmdbSearchResult', () => {
132
- it('uses title/release_date and the movie url path for movie results', () => {
133
- const record = normaliseTmdbSearchResult(
134
- baseSearchResult({ media_type: 'movie', title: 'Dune', release_date: '2021-10-22' })
135
- )
136
- expect(record.title).toBe('Dune')
137
- expect(record.mediaType).toBe('movie')
138
- expect(record.publishedAt).toBe('2021-10-22')
139
- expect(record.sourceUrl).toBe('https://www.themoviedb.org/movie/1')
140
- expect(record.id).toBe('tmdb:movie:1')
141
- })
142
-
143
- it('uses name/first_air_date, tv url path and mediaType tv-show for tv results', () => {
144
- const record = normaliseTmdbSearchResult(
145
- baseSearchResult({
146
- media_type: 'tv',
147
- name: 'Dune: Prophecy',
148
- first_air_date: '2024-11-17',
149
- })
150
- )
151
- expect(record.title).toBe('Dune: Prophecy')
152
- expect(record.mediaType).toBe('tv-show')
153
- expect(record.publishedAt).toBe('2024-11-17')
154
- expect(record.sourceUrl).toBe('https://www.themoviedb.org/tv/1')
155
- expect(record.id).toBe('tmdb:tv:1')
156
- })
157
-
158
- it('defaults title to empty string when the branch-specific field is missing', () => {
159
- const record = normaliseTmdbSearchResult(baseSearchResult({ media_type: 'movie' }))
160
- expect(record.title).toBe('')
161
- })
162
- })
@@ -1,39 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import { hashQuery, makeCacheKey } from './cache'
4
-
5
- describe('makeCacheKey', () => {
6
- it('joins parts with ":"', () => {
7
- expect(makeCacheKey('metadata', 'comicvine', 'issue', '1000')).toBe(
8
- 'metadata:comicvine:issue:1000'
9
- )
10
- })
11
-
12
- it('handles a single part', () => {
13
- expect(makeCacheKey('metadata')).toBe('metadata')
14
- })
15
-
16
- it('handles no parts', () => {
17
- expect(makeCacheKey()).toBe('')
18
- })
19
- })
20
-
21
- describe('hashQuery', () => {
22
- it('is deterministic for the same input', () => {
23
- expect(hashQuery('watchmen')).toBe(hashQuery('watchmen'))
24
- })
25
-
26
- it('differs for different inputs', () => {
27
- expect(hashQuery('watchmen')).not.toBe(hashQuery('v for vendetta'))
28
- })
29
-
30
- it('handles an empty string', () => {
31
- expect(hashQuery('')).toBe('0')
32
- })
33
-
34
- it('returns a non-negative base36 string', () => {
35
- const hash = hashQuery('some query with lots of characters to hash')
36
- expect(hash).toEqual(expect.any(String))
37
- expect(hash.startsWith('-')).toBe(false)
38
- })
39
- })
@@ -1,27 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
2
-
3
- import { MetadataProviderError } from './errors'
4
- import { providerFetch } from './provider-fetch'
5
-
6
- afterEach(() => vi.unstubAllGlobals())
7
-
8
- describe('providerFetch', () => {
9
- it('returns the fetched body', async () => {
10
- vi.stubGlobal(
11
- '$fetch',
12
- vi.fn(async () => ({ ok: true }))
13
- )
14
- await expect(providerFetch('tmdb', 'https://x.test')).resolves.toEqual({ ok: true })
15
- })
16
-
17
- it('wraps failures in a MetadataProviderError for the provider', async () => {
18
- vi.stubGlobal(
19
- '$fetch',
20
- vi.fn(async () => Promise.reject(new Error('boom')))
21
- )
22
- const err = await providerFetch('tmdb', 'https://x.test').catch((e) => e)
23
- expect(err).toBeInstanceOf(MetadataProviderError)
24
- expect(err.provider).toBe('tmdb')
25
- expect(err.message).toContain('API error')
26
- })
27
- })
@@ -1,54 +0,0 @@
1
- import type { MetadataProvider } from '#layers/metadata/shared/types'
2
- import { describe, expect, it } from 'vite-plus/test'
3
-
4
- import { getProvider, getProviders, registerProvider } from './provider-registry'
5
-
6
- function makeProvider(overrides: Partial<MetadataProvider> = {}): MetadataProvider {
7
- return {
8
- id: 'comicvine',
9
- label: 'Comic Vine',
10
- mediaTypes: ['comic'],
11
- search: async () => [],
12
- lookup: async () => null,
13
- ...overrides,
14
- }
15
- }
16
-
17
- // NOTE: _registry is a module-level Map singleton shared across every test in this file
18
- // (vitest isolates modules per test *file*, not per test). Tests below account for
19
- // accumulation by asserting deltas/membership rather than absolute counts, and by
20
- // checking unregistered ids before registering them.
21
-
22
- describe('provider-registry', () => {
23
- it('returns null for an id that has not been registered yet', () => {
24
- expect(getProvider('tmdb')).toBeNull()
25
- })
26
-
27
- it('registers a provider and retrieves it by id', () => {
28
- const provider = makeProvider({ id: 'comicvine' })
29
- registerProvider(provider)
30
- expect(getProvider('comicvine')).toBe(provider)
31
- })
32
-
33
- it('includes registered providers in getProviders()', () => {
34
- const before = getProviders().length
35
- registerProvider(makeProvider({ id: 'openlibrary', label: 'Open Library' }))
36
- const after = getProviders()
37
- expect(after.length).toBe(before + 1)
38
- expect(after.map((p) => p.id)).toContain('openlibrary')
39
- })
40
-
41
- it('overwrites the existing entry when registering the same id again', () => {
42
- const countBefore = getProviders().length
43
- const first = makeProvider({ id: 'google-books', label: 'First' })
44
- const second = makeProvider({ id: 'google-books', label: 'Second' })
45
-
46
- registerProvider(first)
47
- expect(getProviders().length).toBe(countBefore + 1)
48
-
49
- registerProvider(second)
50
- expect(getProviders().length).toBe(countBefore + 1)
51
- expect(getProvider('google-books')).toBe(second)
52
- expect(getProvider('google-books')).not.toBe(first)
53
- })
54
- })
@@ -1,87 +0,0 @@
1
- import type { MetadataProvider, MetadataRecord } from '#layers/metadata/shared/types'
2
- import type * as CacheModule from './cache'
3
- import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
4
-
5
- const { providerState, getCacheRecordMock, setCacheRecordMock } = vi.hoisted(() => ({
6
- providerState: { provider: null as MetadataProvider | null },
7
- getCacheRecordMock: vi.fn(async () => null as { normalised: MetadataRecord } | null),
8
- setCacheRecordMock: vi.fn(async () => undefined),
9
- }))
10
-
11
- vi.mock('./provider-registry', () => ({ getProvider: () => providerState.provider }))
12
- vi.mock('./cache', async (importOriginal) => ({
13
- ...(await importOriginal<typeof CacheModule>()),
14
- getCacheRecord: getCacheRecordMock,
15
- setCacheRecord: setCacheRecordMock,
16
- }))
17
-
18
- import { resolveRecord } from './resolve-record'
19
-
20
- const record = {
21
- id: 'r1',
22
- provider: 'comicvine',
23
- providerId: '42',
24
- mediaType: 'comic',
25
- title: 'Watchmen',
26
- } as MetadataRecord
27
-
28
- const lookup = vi.fn(async () => record as MetadataRecord | null)
29
-
30
- beforeEach(() => {
31
- vi.clearAllMocks()
32
- getCacheRecordMock.mockResolvedValue(null)
33
- lookup.mockResolvedValue(record)
34
- providerState.provider = {
35
- id: 'comicvine',
36
- label: 'Comic Vine',
37
- mediaTypes: ['comic'],
38
- search: async () => [],
39
- lookup,
40
- }
41
- })
42
-
43
- describe('resolveRecord', () => {
44
- it('400s without provider or providerId', async () => {
45
- await expect(resolveRecord({ provider: 'comicvine' })).rejects.toMatchObject({
46
- statusCode: 400,
47
- })
48
- })
49
-
50
- it('404s for an unregistered provider', async () => {
51
- providerState.provider = null
52
- await expect(resolveRecord({ provider: 'nope', providerId: '1' })).rejects.toMatchObject({
53
- statusCode: 404,
54
- })
55
- })
56
-
57
- it('fetches, caches and reports synced on a miss', async () => {
58
- const out = await resolveRecord({ provider: 'comicvine', providerId: '42' })
59
- expect(out).toEqual({ record, synced: true })
60
- expect(setCacheRecordMock).toHaveBeenCalledWith(
61
- 'metadata:comicvine:item:42',
62
- expect.objectContaining({ id: 'r1', providerId: '42', resourceType: 'item' })
63
- )
64
- })
65
-
66
- it('returns the cached record without calling the provider', async () => {
67
- getCacheRecordMock.mockResolvedValue({ normalised: record })
68
- const out = await resolveRecord({ provider: 'comicvine', providerId: '42' })
69
- expect(out).toEqual({ record, synced: false })
70
- expect(lookup).not.toHaveBeenCalled()
71
- })
72
-
73
- it('force skips the cache read', async () => {
74
- getCacheRecordMock.mockResolvedValue({ normalised: record })
75
- const out = await resolveRecord({ provider: 'comicvine', providerId: '42', force: true })
76
- expect(out.synced).toBe(true)
77
- expect(lookup).toHaveBeenCalledOnce()
78
- })
79
-
80
- it('404s when the provider finds nothing and does not cache', async () => {
81
- lookup.mockResolvedValue(null)
82
- await expect(resolveRecord({ provider: 'comicvine', providerId: '42' })).rejects.toMatchObject({
83
- statusCode: 404,
84
- })
85
- expect(setCacheRecordMock).not.toHaveBeenCalled()
86
- })
87
- })