1+ import type { ImageMetadata } from 'astro'
12import { describe , it , expect , beforeEach , vi , afterEach } from 'vitest'
2- import { isDev } from '@lib/config/environmentServer'
3+
4+ /* eslint-disable-next-line no-restricted-syntax -- import.meta.glob required to load fixture assets */
5+ const fixtureModules = import . meta. glob ( '../__fixtures__/avatars/*.{webp,jpg,png}' , {
6+ eager : true ,
7+ import : 'default' ,
8+ } ) as Record < string , ImageMetadata >
9+
10+ const fixtureAvatarNames = Array . from (
11+ new Set (
12+ Object . keys ( fixtureModules )
13+ . map ( ( path ) => path . split ( '/' ) . pop ( ) ?. replace ( / \. ( w e b p | j p g | p n g ) $ / i, '' ) )
14+ . filter ( ( name ) : name is string => Boolean ( name ) )
15+ )
16+ )
17+
18+ const mockLoadAvatarModules = vi . fn ( ( ) => fixtureModules )
19+
20+ vi . mock ( '../avatarImports' , ( ) => ( {
21+ loadAvatarModules : mockLoadAvatarModules ,
22+ } ) )
23+
24+ const primaryAvatar = 'kevin-brown'
25+ const fixtureAvatarCount = fixtureAvatarNames . length
326/**
427 * Comprehensive unit tests for AvatarManager
528 *
@@ -10,9 +33,9 @@ import { isDev } from '@lib/config/environmentServer'
1033 * - Backward-compatible deprecated functions
1134 * - Edge cases and error handling
1235 *
13- * Note: These tests use the actual avatar files in src/assets/images/ avatars/
14- * Expected files: brian-bristol.webp, chris-southam.webp, dru-sellers.webp,
15- * kevin-brown.jpg, kevin-brown.webp, sara-king.webp, test-red-dot.png
36+ * Note: These tests load dedicated fixture avatars from
37+ * src/components/Avatar/server/__fixtures__/avatars/ to keep metadata stable.
38+ * Fixtures mirror the production files except for test-red-dot.png.
1639 */
1740
1841describe ( 'AvatarManager' , ( ) => {
@@ -22,12 +45,15 @@ describe('AvatarManager', () => {
2245 let getAvailableAvatars : any
2346
2447 beforeEach ( async ( ) => {
48+ mockLoadAvatarModules . mockClear ( )
49+ vi . resetModules ( )
50+
2551 // Mock console methods to avoid cluttering test output
2652 vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
2753 vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } )
2854
2955 // Import the module
30- const module = await import ( '../server ' )
56+ const module = await import ( '..' )
3157 AvatarManager = module . AvatarManager
3258
3359 // Get the singleton instance and extract methods for backward compatibility
@@ -50,18 +76,13 @@ describe('AvatarManager', () => {
5076 } )
5177
5278 it ( 'should initialize only once' , ( ) => {
53- const consoleLogSpy = vi . spyOn ( console , 'log' )
79+ const initialCalls = mockLoadAvatarModules . mock . calls . length
80+ expect ( initialCalls ) . toBe ( 1 )
5481
55- // Get instance multiple times
56- AvatarManager . getInstance ( )
5782 AvatarManager . getInstance ( )
5883 AvatarManager . getInstance ( )
5984
60- // Constructor should only run once (if in development mode)
61- // In production, console.log won't be called
62- if ( isDev ( ) ) {
63- expect ( consoleLogSpy ) . toHaveBeenCalledTimes ( 1 )
64- }
85+ expect ( mockLoadAvatarModules ) . toHaveBeenCalledTimes ( initialCalls )
6586 } )
6687
6788 it ( 'should have a private constructor (enforced by TypeScript)' , ( ) => {
@@ -76,7 +97,7 @@ describe('AvatarManager', () => {
7697 describe ( 'getAvatar()' , ( ) => {
7798 it ( 'should return avatar metadata for existing avatar' , ( ) => {
7899 const instance = AvatarManager . getInstance ( )
79- const avatar = instance . getAvatar ( 'kevin-brown' )
100+ const avatar = instance . getAvatar ( primaryAvatar )
80101
81102 expect ( avatar ) . toBeDefined ( )
82103 // Avatar should be an object or a string path
@@ -95,7 +116,7 @@ describe('AvatarManager', () => {
95116 it ( 'should return correct metadata for kevin-brown avatar' , ( ) => {
96117 const instance = AvatarManager . getInstance ( )
97118
98- const kevinAvatar = instance . getAvatar ( 'kevin-brown' )
119+ const kevinAvatar = instance . getAvatar ( primaryAvatar )
99120 expect ( kevinAvatar ) . toBeDefined ( )
100121
101122 // Check the structure of the returned value
@@ -123,7 +144,7 @@ describe('AvatarManager', () => {
123144 it ( 'should be case-sensitive for filenames' , ( ) => {
124145 const instance = AvatarManager . getInstance ( )
125146
126- const lowerCase = instance . getAvatar ( 'kevin-brown' )
147+ const lowerCase = instance . getAvatar ( primaryAvatar )
127148 const upperCase = instance . getAvatar ( 'Kevin-Brown' )
128149
129150 expect ( lowerCase ) . toBeDefined ( )
@@ -135,7 +156,7 @@ describe('AvatarManager', () => {
135156 it ( 'should return true for existing avatar' , ( ) => {
136157 const instance = AvatarManager . getInstance ( )
137158
138- expect ( instance . hasAvatar ( 'kevin-brown' ) ) . toBe ( true )
159+ expect ( instance . hasAvatar ( primaryAvatar ) ) . toBe ( true )
139160 } )
140161
141162 it ( 'should return false for non-existent avatar' , ( ) => {
@@ -154,7 +175,7 @@ describe('AvatarManager', () => {
154175 it ( 'should be case-sensitive' , ( ) => {
155176 const instance = AvatarManager . getInstance ( )
156177
157- expect ( instance . hasAvatar ( 'kevin-brown' ) ) . toBe ( true )
178+ expect ( instance . hasAvatar ( primaryAvatar ) ) . toBe ( true )
158179 expect ( instance . hasAvatar ( 'Kevin-Brown' ) ) . toBe ( false )
159180 expect ( instance . hasAvatar ( 'KEVIN-BROWN' ) ) . toBe ( false )
160181 } )
@@ -166,14 +187,14 @@ describe('AvatarManager', () => {
166187 const avatars = instance . getAvailableAvatars ( )
167188
168189 expect ( Array . isArray ( avatars ) ) . toBe ( true )
169- expect ( avatars . length ) . toBeGreaterThan ( 0 )
190+ expect ( avatars . length ) . toBe ( fixtureAvatarCount )
170191 } )
171192
172193 it ( 'should include kevin-brown avatar' , ( ) => {
173194 const instance = AvatarManager . getInstance ( )
174195 const avatars = instance . getAvailableAvatars ( )
175196
176- expect ( avatars ) . toContain ( 'kevin-brown' )
197+ expect ( avatars ) . toContain ( primaryAvatar )
177198 } )
178199
179200 it ( 'should return filenames without extensions' , ( ) => {
@@ -208,7 +229,7 @@ describe('AvatarManager', () => {
208229 const instance = AvatarManager . getInstance ( )
209230 const allAvatars = instance . getAll ( )
210231
211- expect ( allAvatars [ 'kevin-brown' ] ) . toBeDefined ( )
232+ expect ( allAvatars [ primaryAvatar ] ) . toBeDefined ( )
212233 } )
213234
214235 it ( 'should return a read-only object' , ( ) => {
@@ -224,8 +245,7 @@ describe('AvatarManager', () => {
224245 it ( 'should return the correct number of avatars' , ( ) => {
225246 const instance = AvatarManager . getInstance ( )
226247
227- // Should match the actual number of avatar files in the directory
228- expect ( instance . count ) . toBeGreaterThan ( 0 )
248+ expect ( instance . count ) . toBe ( fixtureAvatarCount )
229249 } )
230250
231251 it ( 'should be consistent with getAvailableAvatars length' , ( ) => {
@@ -261,29 +281,41 @@ describe('AvatarManager', () => {
261281 const instance = AvatarManager . getInstance ( )
262282 const allAvatars = instance . getAll ( )
263283
264- expect ( ( ) => {
265- // Testing runtime immutability
266- delete ( allAvatars as any ) [ 'kevin-brown' ]
267- } ) . toThrow ( )
284+ let deleteResult : boolean | undefined
285+ try {
286+ deleteResult = delete ( allAvatars as any ) [ primaryAvatar ]
287+ } catch ( error ) {
288+ expect ( error ) . toBeInstanceOf ( TypeError )
289+ }
290+
291+ if ( typeof deleteResult === 'boolean' ) {
292+ expect ( deleteResult ) . toBe ( false )
293+ }
294+
295+ expect ( allAvatars [ primaryAvatar ] ) . toBeDefined ( )
268296 } )
269297
270298 it ( 'should not allow modification of existing avatar properties' , ( ) => {
271299 const instance = AvatarManager . getInstance ( )
272300 const allAvatars = instance . getAll ( )
273301
274- expect ( ( ) => {
275- // Testing runtime immutability
276- ; ( allAvatars as any ) [ 'kevin-brown' ] . src = '/modified.jpg'
277- } ) . toThrow ( )
302+ const originalSrc = allAvatars [ primaryAvatar ] ?. src
303+ try {
304+ ; ( allAvatars as any ) [ primaryAvatar ] . src = '/modified.jpg'
305+ } catch ( error ) {
306+ expect ( error ) . toBeInstanceOf ( TypeError )
307+ }
308+
309+ expect ( allAvatars [ primaryAvatar ] ?. src ) . toBe ( originalSrc )
278310 } )
279311 } )
280312
281313 describe ( 'Backward Compatibility' , ( ) => {
282314 describe ( 'getAvatarImage() (deprecated)' , ( ) => {
283315 it ( 'should work the same as getInstance().getAvatar()' , ( ) => {
284316 const instance = AvatarManager . getInstance ( )
285- const newWay = instance . getAvatar ( 'kevin-brown' )
286- const oldWay = getAvatarImage ( 'kevin-brown' )
317+ const newWay = instance . getAvatar ( primaryAvatar )
318+ const oldWay = getAvatarImage ( primaryAvatar )
287319
288320 expect ( oldWay ) . toEqual ( newWay )
289321 } )
@@ -319,24 +351,23 @@ describe('AvatarManager', () => {
319351 const avatars = getAvailableAvatars ( )
320352
321353 expect ( Array . isArray ( avatars ) ) . toBe ( true )
322- expect ( avatars . length ) . toBeGreaterThan ( 0 )
354+ expect ( avatars . length ) . toBe ( fixtureAvatarCount )
323355 } )
324356 } )
325357 } )
326358
327359 describe ( 'Edge Cases' , ( ) => {
328360 it ( 'should handle avatar with special characters in filename' , ( ) => {
329- // This would need mock data with special chars, skipping for now
330- // but good to consider for real-world usage
331361 const instance = AvatarManager . getInstance ( )
332- expect ( instance . hasAvatar ( 'avatar-with-dash' ) ) . toBeDefined ( )
362+ const hyphenatedName = fixtureAvatarNames . find ( ( name ) => name . includes ( '-' ) ) ?? primaryAvatar
363+ expect ( instance . hasAvatar ( hyphenatedName ) ) . toBe ( true )
333364 } )
334365
335366 it ( 'should handle multiple file extensions correctly' , ( ) => {
336367 const instance = AvatarManager . getInstance ( )
337368
338- // kevin-brown should be found regardless of extension
339- expect ( instance . hasAvatar ( 'kevin-brown' ) ) . toBe ( true )
369+ // The canonical avatar should be found regardless of extension
370+ expect ( instance . hasAvatar ( primaryAvatar ) ) . toBe ( true )
340371 } )
341372
342373 it ( 'should strip file extension case-insensitively' , ( ) => {
@@ -356,7 +387,7 @@ describe('AvatarManager', () => {
356387 describe ( 'Type Safety' , ( ) => {
357388 it ( 'should return proper ImageMetadata type from getAvatar' , ( ) => {
358389 const instance = AvatarManager . getInstance ( )
359- const avatar = instance . getAvatar ( 'kevin-brown' )
390+ const avatar = instance . getAvatar ( primaryAvatar )
360391
361392 if ( avatar && typeof avatar === 'object' ) {
362393 expect ( typeof avatar . src ) . toBe ( 'string' )
@@ -397,13 +428,13 @@ describe('AvatarManager', () => {
397428
398429 // Make many rapid calls
399430 for ( let i = 0 ; i < 100 ; i ++ ) {
400- instance . getAvatar ( 'kevin-brown' )
401- instance . hasAvatar ( 'kevin-brown' )
431+ instance . getAvatar ( primaryAvatar )
432+ instance . hasAvatar ( primaryAvatar )
402433 instance . getAvailableAvatars ( )
403434 }
404435
405436 // Should still work correctly
406- expect ( instance . getAvatar ( 'kevin-brown' ) ) . toBeDefined ( )
437+ expect ( instance . getAvatar ( primaryAvatar ) ) . toBeDefined ( )
407438 expect ( instance . count ) . toBeGreaterThan ( 0 )
408439 } )
409440 } )
0 commit comments