1+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
2+ import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+ import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
4+ import { executeRender } from '@test/unit/helpers/litRuntime'
5+ import SkillsFixture from '@components/Home/Skills/client/__fixtures__/skillsCarousel.fixture.astro'
6+ import type { SkillsCarouselElement } from '@components/Home/Skills/client'
7+
8+ type SkillsModule = WebComponentModule < SkillsCarouselElement >
9+
10+ const addButtonEventListenersMock = vi . hoisted ( ( ) => vi . fn ( ) )
11+ const createAnimationControllerMock = vi . hoisted ( ( ) => vi . fn ( ) )
12+
13+ type AutoplayPluginInstance = {
14+ play : ReturnType < typeof vi . fn >
15+ stop : ReturnType < typeof vi . fn >
16+ }
17+
18+ const autoplayPluginInstances : AutoplayPluginInstance [ ] = [ ]
19+
20+ const createAutoplayPluginMock = vi . fn ( ( ) => {
21+ const instance : AutoplayPluginInstance = {
22+ play : vi . fn ( ) ,
23+ stop : vi . fn ( ) ,
24+ }
25+ autoplayPluginInstances . push ( instance )
26+ return instance
27+ } )
28+
29+ vi . mock ( '@components/scripts/store' , ( ) => ( {
30+ createAnimationController : createAnimationControllerMock ,
31+ } ) )
32+
33+ vi . mock ( '@components/scripts/elementListeners' , ( ) => ( {
34+ addButtonEventListeners : addButtonEventListenersMock ,
35+ } ) )
36+
37+ vi . mock ( 'embla-carousel' , ( ) => {
38+ const createEmbla = vi . fn ( ( ) => {
39+ const handlers = new Map < string , Array < ( ) => void > > ( )
40+ let selectedIndex = 0
41+
42+ const api = {
43+ canScrollPrev : vi . fn ( ( ) => false ) ,
44+ canScrollNext : vi . fn ( ( ) => false ) ,
45+ scrollPrev : vi . fn ( ) ,
46+ scrollNext : vi . fn ( ) ,
47+ on : vi . fn ( ( event : string , handler : ( ) => void ) => {
48+ const existing = handlers . get ( event ) ?? [ ]
49+ handlers . set ( event , [ ...existing , handler ] )
50+ return api
51+ } ) ,
52+ off : vi . fn ( ( event : string , handler : ( ) => void ) => {
53+ const existing = handlers . get ( event ) ?? [ ]
54+ handlers . set (
55+ event ,
56+ existing . filter ( entry => entry !== handler )
57+ )
58+ return api
59+ } ) ,
60+ destroy : vi . fn ( ) ,
61+ scrollSnapList : vi . fn ( ( ) => [ 0 , 1 , 2 ] ) ,
62+ selectedScrollSnap : vi . fn ( ( ) => selectedIndex ) ,
63+ scrollTo : vi . fn ( ( index : number ) => {
64+ selectedIndex = index
65+ } ) ,
66+ }
67+
68+ return api
69+ } )
70+
71+ return {
72+ default : createEmbla ,
73+ }
74+ } )
75+
76+ vi . mock ( 'embla-carousel-autoplay' , ( ) => ( {
77+ __esModule : true ,
78+ default : createAutoplayPluginMock ,
79+ } ) )
80+
81+ describe ( 'Skills carousel component' , ( ) => {
82+ let container : AstroContainer
83+
84+ beforeEach ( async ( ) => {
85+ container = await AstroContainer . create ( )
86+ vi . clearAllMocks ( )
87+ autoplayPluginInstances . length = 0
88+ createAutoplayPluginMock . mockClear ( )
89+ createAnimationControllerMock . mockReturnValue ( {
90+ requestPlay : vi . fn ( ) ,
91+ requestPause : vi . fn ( ) ,
92+ setInstancePauseState : vi . fn ( ) ,
93+ clearUserPreference : vi . fn ( ) ,
94+ destroy : vi . fn ( ) ,
95+ } )
96+ } )
97+
98+ afterEach ( ( ) => {
99+ vi . restoreAllMocks ( )
100+ vi . useRealTimers ( )
101+ } )
102+
103+ const renderSkills = async (
104+ assertion : ( _context : { element : SkillsCarouselElement } ) => Promise < void > | void
105+ ) : Promise < void > => {
106+ await executeRender < SkillsModule > ( {
107+ container,
108+ component : SkillsFixture ,
109+ moduleSpecifier : '@components/Home/Skills/client/index' ,
110+ selector : 'skills-carousel' ,
111+ waitForReady : async ( element : SkillsCarouselElement ) => {
112+ await element . updateComplete
113+ } ,
114+ assert : async ( { element, module, renderResult } ) => {
115+ expect ( renderResult ) . toContain ( `<${ module . registeredName } ` )
116+ await assertion ( { element } )
117+ } ,
118+ } )
119+ }
120+
121+ it ( 'renders centered labels with nav controls and no dots' , async ( ) => {
122+ await renderSkills ( async ( { element } ) => {
123+ const firstLink = element . querySelector ( 'a' )
124+ expect ( firstLink ?. classList . contains ( 'flex-col' ) ) . toBe ( true )
125+ expect ( firstLink ?. classList . contains ( 'items-center' ) ) . toBe ( true )
126+ expect ( firstLink ?. classList . contains ( 'text-center' ) ) . toBe ( true )
127+
128+ expect ( element . querySelector ( '[data-skills-prev]' ) ) . toBeTruthy ( )
129+ expect ( element . querySelector ( '[data-skills-next]' ) ) . toBeTruthy ( )
130+ expect ( element . querySelector ( '[data-skills-autoplay-toggle]' ) ) . toBeTruthy ( )
131+ expect ( element . querySelector ( '[data-skills-dots]' ) ) . toBeFalsy ( )
132+ } )
133+ } )
134+
135+ it ( 'toggles autoplay state via the play/pause control' , async ( ) => {
136+ vi . useFakeTimers ( )
137+ const buttonHandlers = new Map < Element , ( ) => void > ( )
138+ addButtonEventListenersMock . mockImplementation ( ( button , handler ) => {
139+ buttonHandlers . set ( button as Element , handler as ( ) => void )
140+ } )
141+
142+ await renderSkills ( async ( { element } ) => {
143+ const toggle = element . querySelector ( '[data-skills-autoplay-toggle]' ) as HTMLButtonElement
144+ expect ( toggle ) . toBeTruthy ( )
145+
146+ const handler = buttonHandlers . get ( toggle )
147+ expect ( handler ) . toBeTruthy ( )
148+
149+ const autoplayInstance = autoplayPluginInstances [ 0 ]
150+ expect ( autoplayInstance ) . toBeTruthy ( )
151+
152+ vi . runAllTimers ( )
153+ expect ( autoplayInstance ?. play ) . toHaveBeenCalled ( )
154+ expect ( toggle . getAttribute ( 'aria-label' ) ) . toBe ( 'Pause skills' )
155+
156+ handler ?.( )
157+ expect ( autoplayInstance ?. stop ) . toHaveBeenCalled ( )
158+ expect ( toggle . getAttribute ( 'aria-label' ) ) . toBe ( 'Play skills' )
159+
160+ handler ?.( )
161+ vi . runAllTimers ( )
162+ expect ( autoplayInstance ?. play ) . toHaveBeenCalled ( )
163+ expect ( toggle . getAttribute ( 'aria-label' ) ) . toBe ( 'Pause skills' )
164+ } )
165+ } )
166+ } )
0 commit comments