mtrl 0.3.0 → 0.3.1

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 (60) hide show
  1. package/CLAUDE.md +33 -0
  2. package/index.ts +0 -2
  3. package/package.json +3 -1
  4. package/src/components/navigation/index.ts +4 -1
  5. package/src/components/navigation/types.ts +33 -0
  6. package/src/components/snackbar/index.ts +7 -1
  7. package/src/components/snackbar/types.ts +25 -0
  8. package/src/components/switch/index.ts +5 -1
  9. package/src/components/switch/types.ts +13 -0
  10. package/src/components/textfield/index.ts +7 -1
  11. package/src/components/textfield/types.ts +36 -0
  12. package/test/components/badge.test.ts +545 -0
  13. package/test/components/bottom-app-bar.test.ts +303 -0
  14. package/test/components/button.test.ts +233 -0
  15. package/test/components/card.test.ts +560 -0
  16. package/test/components/carousel.test.ts +951 -0
  17. package/test/components/checkbox.test.ts +462 -0
  18. package/test/components/chip.test.ts +692 -0
  19. package/test/components/datepicker.test.ts +1124 -0
  20. package/test/components/dialog.test.ts +990 -0
  21. package/test/components/divider.test.ts +412 -0
  22. package/test/components/extended-fab.test.ts +672 -0
  23. package/test/components/fab.test.ts +561 -0
  24. package/test/components/list.test.ts +365 -0
  25. package/test/components/menu.test.ts +718 -0
  26. package/test/components/navigation.test.ts +186 -0
  27. package/test/components/progress.test.ts +567 -0
  28. package/test/components/radios.test.ts +699 -0
  29. package/test/components/search.test.ts +1135 -0
  30. package/test/components/segmented-button.test.ts +732 -0
  31. package/test/components/sheet.test.ts +641 -0
  32. package/test/components/slider.test.ts +1220 -0
  33. package/test/components/snackbar.test.ts +461 -0
  34. package/test/components/switch.test.ts +452 -0
  35. package/test/components/tabs.test.ts +1369 -0
  36. package/test/components/textfield.test.ts +400 -0
  37. package/test/components/timepicker.test.ts +592 -0
  38. package/test/components/tooltip.test.ts +630 -0
  39. package/test/components/top-app-bar.test.ts +566 -0
  40. package/test/core/dom.attributes.test.ts +148 -0
  41. package/test/core/dom.classes.test.ts +152 -0
  42. package/test/core/dom.events.test.ts +243 -0
  43. package/test/core/emitter.test.ts +141 -0
  44. package/test/core/ripple.test.ts +99 -0
  45. package/test/core/state.store.test.ts +189 -0
  46. package/test/core/utils.normalize.test.ts +61 -0
  47. package/test/core/utils.object.test.ts +120 -0
  48. package/test/setup.ts +451 -0
  49. package/tsconfig.json +2 -2
  50. package/src/components/snackbar/constants.ts +0 -26
  51. package/test/components/button.test.js +0 -170
  52. package/test/components/checkbox.test.js +0 -238
  53. package/test/components/list.test.js +0 -105
  54. package/test/components/menu.test.js +0 -385
  55. package/test/components/navigation.test.js +0 -227
  56. package/test/components/snackbar.test.js +0 -234
  57. package/test/components/switch.test.js +0 -186
  58. package/test/components/textfield.test.js +0 -314
  59. package/test/core/emitter.test.js +0 -141
  60. package/test/core/ripple.test.js +0 -66
@@ -0,0 +1,951 @@
1
+ // test/components/carousel.test.ts
2
+ import { describe, test, expect, mock, beforeAll, afterAll } from 'bun:test';
3
+ import { JSDOM } from 'jsdom';
4
+ import {
5
+ type CarouselComponent,
6
+ type CarouselConfig,
7
+ type CarouselSlide,
8
+ type CarouselLayout,
9
+ type CarouselScrollBehavior
10
+ } from '../../src/components/carousel/types';
11
+
12
+ // Setup jsdom environment
13
+ let dom: JSDOM;
14
+ let window: Window;
15
+ let document: Document;
16
+ let originalGlobalDocument: any;
17
+ let originalGlobalWindow: any;
18
+
19
+ beforeAll(() => {
20
+ // Create a new JSDOM instance
21
+ dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
22
+ url: 'http://localhost/',
23
+ pretendToBeVisual: true
24
+ });
25
+
26
+ // Get window and document from jsdom
27
+ window = dom.window;
28
+ document = window.document;
29
+
30
+ // Store original globals
31
+ originalGlobalDocument = global.document;
32
+ originalGlobalWindow = global.window;
33
+
34
+ // Set globals to use jsdom
35
+ global.document = document;
36
+ global.window = window;
37
+ global.Element = window.Element;
38
+ global.HTMLElement = window.HTMLElement;
39
+ global.HTMLButtonElement = window.HTMLButtonElement;
40
+ global.Event = window.Event;
41
+ });
42
+
43
+ afterAll(() => {
44
+ // Restore original globals
45
+ global.document = originalGlobalDocument;
46
+ global.window = originalGlobalWindow;
47
+
48
+ // Clean up jsdom
49
+ window.close();
50
+ });
51
+
52
+ // Mock carousel implementation
53
+ const createMockCarousel = (config: CarouselConfig = {}): CarouselComponent => {
54
+ const element = document.createElement('div');
55
+ element.className = 'mtrl-carousel';
56
+
57
+ // Default settings
58
+ const settings = {
59
+ initialSlide: config.initialSlide || 0,
60
+ loop: config.loop !== undefined ? config.loop : true,
61
+ transition: config.transition || 'slide',
62
+ transitionDuration: config.transitionDuration || 300,
63
+ borderRadius: config.borderRadius || 16,
64
+ gap: config.gap || 8,
65
+ layout: config.layout || 'multi-browse',
66
+ scrollBehavior: config.scrollBehavior || 'snap',
67
+ centered: config.centered || false,
68
+ componentName: config.componentName || 'carousel',
69
+ prefix: config.prefix || 'mtrl'
70
+ };
71
+
72
+ // Apply additional classes
73
+ if (config.class) {
74
+ const classes = config.class.split(' ');
75
+ classes.forEach(className => element.classList.add(className));
76
+ }
77
+
78
+ // Apply layout class
79
+ if (settings.layout) {
80
+ element.classList.add(`mtrl-carousel--${settings.layout}`);
81
+ }
82
+
83
+ // Apply scrollBehavior class
84
+ if (settings.scrollBehavior) {
85
+ element.classList.add(`mtrl-carousel--${settings.scrollBehavior}`);
86
+ }
87
+
88
+ // Create slides container
89
+ const slidesContainer = document.createElement('div');
90
+ slidesContainer.className = 'mtrl-carousel__slides';
91
+
92
+ // Create slides elements
93
+ const slideElements: HTMLElement[] = [];
94
+ const slides: CarouselSlide[] = config.slides || [];
95
+
96
+ // Setup slides
97
+ slides.forEach((slide, index) => {
98
+ const slideElement = document.createElement('div');
99
+ slideElement.className = 'mtrl-carousel__slide';
100
+ slideElement.setAttribute('data-index', index.toString());
101
+
102
+ // Add slide content
103
+ if (slide.image) {
104
+ const img = document.createElement('img');
105
+ img.className = 'mtrl-carousel__image';
106
+ img.src = slide.image;
107
+ img.alt = slide.title || `Slide ${index + 1}`;
108
+ slideElement.appendChild(img);
109
+ }
110
+
111
+ if (slide.title) {
112
+ const title = document.createElement('h3');
113
+ title.className = 'mtrl-carousel__title';
114
+ title.textContent = slide.title;
115
+ slideElement.appendChild(title);
116
+ }
117
+
118
+ if (slide.description) {
119
+ const desc = document.createElement('p');
120
+ desc.className = 'mtrl-carousel__description';
121
+ desc.textContent = slide.description;
122
+ slideElement.appendChild(desc);
123
+ }
124
+
125
+ if (slide.buttonText && slide.buttonUrl) {
126
+ const button = document.createElement('a');
127
+ button.className = 'mtrl-carousel__button';
128
+ button.textContent = slide.buttonText;
129
+ button.href = slide.buttonUrl;
130
+ slideElement.appendChild(button);
131
+ }
132
+
133
+ // Apply accent color if available
134
+ if (slide.accent) {
135
+ slideElement.style.setProperty('--carousel-accent-color', slide.accent);
136
+ }
137
+
138
+ // Apply size class if specified
139
+ if (slide.size) {
140
+ slideElement.classList.add(`mtrl-carousel__slide--${slide.size}`);
141
+ }
142
+
143
+ slidesContainer.appendChild(slideElement);
144
+ slideElements.push(slideElement);
145
+ });
146
+
147
+ element.appendChild(slidesContainer);
148
+
149
+ // Create navigation elements
150
+ const prevButton = document.createElement('button');
151
+ prevButton.className = 'mtrl-carousel__prev';
152
+ prevButton.setAttribute('aria-label', 'Previous slide');
153
+
154
+ const nextButton = document.createElement('button');
155
+ nextButton.className = 'mtrl-carousel__next';
156
+ nextButton.setAttribute('aria-label', 'Next slide');
157
+
158
+ element.appendChild(prevButton);
159
+ element.appendChild(nextButton);
160
+
161
+ // Create indicators
162
+ const indicators = document.createElement('div');
163
+ indicators.className = 'mtrl-carousel__indicators';
164
+
165
+ slides.forEach((_, index) => {
166
+ const indicator = document.createElement('button');
167
+ indicator.className = 'mtrl-carousel__indicator';
168
+ indicator.setAttribute('data-index', index.toString());
169
+ indicator.setAttribute('aria-label', `Go to slide ${index + 1}`);
170
+
171
+ if (index === settings.initialSlide) {
172
+ indicator.classList.add('mtrl-carousel__indicator--active');
173
+ }
174
+
175
+ indicators.appendChild(indicator);
176
+ });
177
+
178
+ element.appendChild(indicators);
179
+
180
+ // Create Show All link if needed
181
+ if (config.showAllLink !== false) {
182
+ const showAllLink = document.createElement('a');
183
+ showAllLink.className = 'mtrl-carousel__show-all';
184
+ showAllLink.textContent = 'Show all';
185
+ showAllLink.href = '#';
186
+ showAllLink.addEventListener('click', (e) => {
187
+ e.preventDefault();
188
+ if (config.onShowAll) {
189
+ config.onShowAll();
190
+ }
191
+ });
192
+
193
+ element.appendChild(showAllLink);
194
+ }
195
+
196
+ // Set up current slide
197
+ let currentSlide = settings.initialSlide;
198
+
199
+ // Set active slide
200
+ const setActiveSlide = (index: number) => {
201
+ // Update indicators
202
+ const allIndicators = indicators.querySelectorAll('.mtrl-carousel__indicator');
203
+ allIndicators.forEach((ind) => ind.classList.remove('mtrl-carousel__indicator--active'));
204
+
205
+ const activeIndicator = indicators.querySelector(`.mtrl-carousel__indicator[data-index="${index}"]`);
206
+ if (activeIndicator) {
207
+ activeIndicator.classList.add('mtrl-carousel__indicator--active');
208
+ }
209
+
210
+ // Update carousel position
211
+ slidesContainer.style.transform = `translateX(-${index * 100}%)`;
212
+
213
+ // Update current slide
214
+ currentSlide = index;
215
+ };
216
+
217
+ // Set initial active slide
218
+ setActiveSlide(currentSlide);
219
+
220
+ // Event handlers
221
+ const eventHandlers: Record<string, Function[]> = {};
222
+
223
+ const emit = (event: string, data?: any) => {
224
+ if (eventHandlers[event]) {
225
+ eventHandlers[event].forEach(handler => handler(data));
226
+ }
227
+ };
228
+
229
+ // Navigation event listeners
230
+ prevButton.addEventListener('click', () => {
231
+ carousel.prev();
232
+ });
233
+
234
+ nextButton.addEventListener('click', () => {
235
+ carousel.next();
236
+ });
237
+
238
+ // Indicator event listeners
239
+ const indicatorButtons = indicators.querySelectorAll('.mtrl-carousel__indicator');
240
+ indicatorButtons.forEach(button => {
241
+ button.addEventListener('click', () => {
242
+ const index = parseInt(button.getAttribute('data-index') || '0');
243
+ carousel.goTo(index);
244
+ });
245
+ });
246
+
247
+ // Create the SlidesAPI
248
+ const slidesAPI = {
249
+ addSlide: (slide: CarouselSlide, index?: number) => {
250
+ const slideElement = document.createElement('div');
251
+ slideElement.className = 'mtrl-carousel__slide';
252
+
253
+ // Add slide content
254
+ if (slide.image) {
255
+ const img = document.createElement('img');
256
+ img.className = 'mtrl-carousel__image';
257
+ img.src = slide.image;
258
+ img.alt = slide.title || `Slide ${slides.length + 1}`;
259
+ slideElement.appendChild(img);
260
+ }
261
+
262
+ if (slide.title) {
263
+ const title = document.createElement('h3');
264
+ title.className = 'mtrl-carousel__title';
265
+ title.textContent = slide.title;
266
+ slideElement.appendChild(title);
267
+ }
268
+
269
+ // Insert slide at specified index or append
270
+ if (index !== undefined && index >= 0 && index <= slides.length) {
271
+ slides.splice(index, 0, slide);
272
+ const insertBeforeElement = index < slideElements.length ? slideElements[index] : null;
273
+
274
+ if (insertBeforeElement) {
275
+ slidesContainer.insertBefore(slideElement, insertBeforeElement);
276
+ slideElements.splice(index, 0, slideElement);
277
+ } else {
278
+ slidesContainer.appendChild(slideElement);
279
+ slideElements.push(slideElement);
280
+ }
281
+ } else {
282
+ slides.push(slide);
283
+ slidesContainer.appendChild(slideElement);
284
+ slideElements.push(slideElement);
285
+ }
286
+
287
+ // Update data-index attributes
288
+ slideElements.forEach((el, i) => {
289
+ el.setAttribute('data-index', i.toString());
290
+ });
291
+
292
+ // Create new indicator
293
+ const indicator = document.createElement('button');
294
+ indicator.className = 'mtrl-carousel__indicator';
295
+ indicator.setAttribute('data-index', (slides.length - 1).toString());
296
+ indicator.setAttribute('aria-label', `Go to slide ${slides.length}`);
297
+
298
+ indicator.addEventListener('click', () => {
299
+ const indicatorIndex = parseInt(indicator.getAttribute('data-index') || '0');
300
+ carousel.goTo(indicatorIndex);
301
+ });
302
+
303
+ indicators.appendChild(indicator);
304
+
305
+ // Emit change event
306
+ emit('slideAdded', { slide, index });
307
+
308
+ return slidesAPI;
309
+ },
310
+
311
+ removeSlide: (index: number) => {
312
+ if (index >= 0 && index < slides.length) {
313
+ // Remove slide from arrays
314
+ const [removedSlide] = slides.splice(index, 1);
315
+ const [removedElement] = slideElements.splice(index, 1);
316
+
317
+ // Remove slide element from DOM
318
+ slidesContainer.removeChild(removedElement);
319
+
320
+ // Remove indicator
321
+ const indicatorToRemove = indicators.querySelector(`.mtrl-carousel__indicator[data-index="${index}"]`);
322
+ if (indicatorToRemove) {
323
+ indicators.removeChild(indicatorToRemove);
324
+ }
325
+
326
+ // Update remaining indicators
327
+ const remainingIndicators = indicators.querySelectorAll('.mtrl-carousel__indicator');
328
+ remainingIndicators.forEach((ind, i) => {
329
+ ind.setAttribute('data-index', i.toString());
330
+ ind.setAttribute('aria-label', `Go to slide ${i + 1}`);
331
+ });
332
+
333
+ // Update data-index attributes on remaining slides
334
+ slideElements.forEach((el, i) => {
335
+ el.setAttribute('data-index', i.toString());
336
+ });
337
+
338
+ // Adjust current slide if necessary
339
+ if (currentSlide >= slides.length) {
340
+ const newCurrentSlide = Math.max(0, slides.length - 1);
341
+ setActiveSlide(newCurrentSlide);
342
+ }
343
+
344
+ // Emit change event
345
+ emit('slideRemoved', { slide: removedSlide, index });
346
+ }
347
+
348
+ return slidesAPI;
349
+ },
350
+
351
+ updateSlide: (index: number, slide: CarouselSlide) => {
352
+ if (index >= 0 && index < slides.length) {
353
+ slides[index] = { ...slides[index], ...slide };
354
+ const slideElement = slideElements[index];
355
+
356
+ // Update image if provided
357
+ if (slide.image) {
358
+ let img = slideElement.querySelector('.mtrl-carousel__image');
359
+ if (!img) {
360
+ img = document.createElement('img');
361
+ img.className = 'mtrl-carousel__image';
362
+ slideElement.appendChild(img);
363
+ }
364
+ (img as HTMLImageElement).src = slide.image;
365
+ (img as HTMLImageElement).alt = slide.title || `Slide ${index + 1}`;
366
+ }
367
+
368
+ // Update title if provided
369
+ if (slide.title !== undefined) {
370
+ let title = slideElement.querySelector('.mtrl-carousel__title');
371
+ if (!title && slide.title) {
372
+ title = document.createElement('h3');
373
+ title.className = 'mtrl-carousel__title';
374
+ slideElement.appendChild(title);
375
+ }
376
+
377
+ if (title) {
378
+ if (slide.title) {
379
+ title.textContent = slide.title;
380
+ } else {
381
+ slideElement.removeChild(title);
382
+ }
383
+ }
384
+ }
385
+
386
+ // Update accent color if provided
387
+ if (slide.accent) {
388
+ slideElement.style.setProperty('--carousel-accent-color', slide.accent);
389
+ }
390
+
391
+ // Emit change event
392
+ emit('slideUpdated', { slide, index });
393
+ }
394
+
395
+ return slidesAPI;
396
+ },
397
+
398
+ getSlide: (index: number) => {
399
+ return (index >= 0 && index < slides.length) ? slides[index] : null;
400
+ },
401
+
402
+ getCount: () => {
403
+ return slides.length;
404
+ },
405
+
406
+ getElements: () => {
407
+ return [...slideElements];
408
+ }
409
+ };
410
+
411
+ // Create the carousel component
412
+ const carousel: CarouselComponent = {
413
+ element,
414
+ slides: slidesAPI,
415
+
416
+ lifecycle: {
417
+ destroy: () => {
418
+ // Clean up event listeners and remove element
419
+ carousel.destroy();
420
+ }
421
+ },
422
+
423
+ getClass: (name: string) => {
424
+ return `${settings.prefix}-${name}`;
425
+ },
426
+
427
+ next: () => {
428
+ let nextSlide = currentSlide + 1;
429
+
430
+ if (nextSlide >= slides.length) {
431
+ if (settings.loop) {
432
+ nextSlide = 0;
433
+ } else {
434
+ nextSlide = slides.length - 1;
435
+ }
436
+ }
437
+
438
+ setActiveSlide(nextSlide);
439
+ emit('slideChange', { current: nextSlide, previous: currentSlide });
440
+
441
+ return carousel;
442
+ },
443
+
444
+ prev: () => {
445
+ let prevSlide = currentSlide - 1;
446
+
447
+ if (prevSlide < 0) {
448
+ if (settings.loop) {
449
+ prevSlide = slides.length - 1;
450
+ } else {
451
+ prevSlide = 0;
452
+ }
453
+ }
454
+
455
+ setActiveSlide(prevSlide);
456
+ emit('slideChange', { current: prevSlide, previous: currentSlide });
457
+
458
+ return carousel;
459
+ },
460
+
461
+ goTo: (index: number) => {
462
+ if (index >= 0 && index < slides.length) {
463
+ const previousSlide = currentSlide;
464
+ setActiveSlide(index);
465
+ emit('slideChange', { current: index, previous: previousSlide });
466
+ }
467
+
468
+ return carousel;
469
+ },
470
+
471
+ getCurrentSlide: () => {
472
+ return currentSlide;
473
+ },
474
+
475
+ addSlide: (slide: CarouselSlide, index?: number) => {
476
+ slidesAPI.addSlide(slide, index);
477
+ return carousel;
478
+ },
479
+
480
+ removeSlide: (index: number) => {
481
+ slidesAPI.removeSlide(index);
482
+ return carousel;
483
+ },
484
+
485
+ enableLoop: () => {
486
+ settings.loop = true;
487
+ return carousel;
488
+ },
489
+
490
+ disableLoop: () => {
491
+ settings.loop = false;
492
+ return carousel;
493
+ },
494
+
495
+ setBorderRadius: (radius: number) => {
496
+ settings.borderRadius = radius;
497
+ element.style.setProperty('--carousel-border-radius', `${radius}px`);
498
+ return carousel;
499
+ },
500
+
501
+ setGap: (gap: number) => {
502
+ settings.gap = gap;
503
+ element.style.setProperty('--carousel-gap', `${gap}px`);
504
+ return carousel;
505
+ },
506
+
507
+ destroy: () => {
508
+ // Clean up event listeners
509
+ prevButton.removeEventListener('click', carousel.prev);
510
+ nextButton.removeEventListener('click', carousel.next);
511
+
512
+ // Remove the element from the DOM if it has a parent
513
+ if (element.parentNode) {
514
+ element.parentNode.removeChild(element);
515
+ }
516
+
517
+ // Clear event handlers
518
+ for (const event in eventHandlers) {
519
+ eventHandlers[event] = [];
520
+ }
521
+ },
522
+
523
+ on: (event: string, handler: Function) => {
524
+ if (!eventHandlers[event]) {
525
+ eventHandlers[event] = [];
526
+ }
527
+ eventHandlers[event].push(handler);
528
+ return carousel;
529
+ },
530
+
531
+ off: (event: string, handler: Function) => {
532
+ if (eventHandlers[event]) {
533
+ eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
534
+ }
535
+ return carousel;
536
+ },
537
+
538
+ addClass: (...classes: string[]) => {
539
+ classes.forEach(className => element.classList.add(className));
540
+ return carousel;
541
+ }
542
+ };
543
+
544
+ // Apply initial settings
545
+ carousel.setBorderRadius(settings.borderRadius);
546
+ carousel.setGap(settings.gap);
547
+
548
+ return carousel;
549
+ };
550
+
551
+ describe('Carousel Component', () => {
552
+ test('should create a carousel element', () => {
553
+ const carousel = createMockCarousel();
554
+ expect(carousel.element).toBeDefined();
555
+ expect(carousel.element.tagName).toBe('DIV');
556
+ expect(carousel.element.className).toContain('mtrl-carousel');
557
+ });
558
+
559
+ test('should create carousel with specified layout', () => {
560
+ const layouts: CarouselLayout[] = ['multi-browse', 'uncontained', 'hero', 'full-screen'];
561
+
562
+ layouts.forEach(layout => {
563
+ const carousel = createMockCarousel({ layout });
564
+ expect(carousel.element.className).toContain(`mtrl-carousel--${layout}`);
565
+ });
566
+ });
567
+
568
+ test('should create carousel with specified scroll behavior', () => {
569
+ const behaviors: CarouselScrollBehavior[] = ['default', 'snap'];
570
+
571
+ behaviors.forEach(behavior => {
572
+ const carousel = createMockCarousel({ scrollBehavior: behavior });
573
+ expect(carousel.element.className).toContain(`mtrl-carousel--${behavior}`);
574
+ });
575
+ });
576
+
577
+ test('should create carousel with slides', () => {
578
+ const slides: CarouselSlide[] = [
579
+ { image: 'image1.jpg', title: 'Slide 1', description: 'Description 1' },
580
+ { image: 'image2.jpg', title: 'Slide 2', description: 'Description 2' },
581
+ { image: 'image3.jpg', title: 'Slide 3', description: 'Description 3' }
582
+ ];
583
+
584
+ const carousel = createMockCarousel({ slides });
585
+
586
+ const slideElements = carousel.element.querySelectorAll('.mtrl-carousel__slide');
587
+ expect(slideElements.length).toBe(3);
588
+
589
+ const imageElements = carousel.element.querySelectorAll('.mtrl-carousel__image');
590
+ expect(imageElements.length).toBe(3);
591
+
592
+ const titleElements = carousel.element.querySelectorAll('.mtrl-carousel__title');
593
+ expect(titleElements.length).toBe(3);
594
+ expect(titleElements[0].textContent).toBe('Slide 1');
595
+ expect(titleElements[1].textContent).toBe('Slide 2');
596
+ expect(titleElements[2].textContent).toBe('Slide 3');
597
+ });
598
+
599
+ test('should create navigation controls', () => {
600
+ const carousel = createMockCarousel();
601
+
602
+ const prevButton = carousel.element.querySelector('.mtrl-carousel__prev');
603
+ expect(prevButton).toBeDefined();
604
+
605
+ const nextButton = carousel.element.querySelector('.mtrl-carousel__next');
606
+ expect(nextButton).toBeDefined();
607
+ });
608
+
609
+ test('should create slide indicators', () => {
610
+ const slides: CarouselSlide[] = [
611
+ { image: 'image1.jpg' },
612
+ { image: 'image2.jpg' },
613
+ { image: 'image3.jpg' }
614
+ ];
615
+
616
+ const carousel = createMockCarousel({ slides });
617
+
618
+ const indicators = carousel.element.querySelectorAll('.mtrl-carousel__indicator');
619
+ expect(indicators.length).toBe(3);
620
+ });
621
+
622
+ test('should show "Show All" link by default', () => {
623
+ const carousel = createMockCarousel();
624
+ const showAllLink = carousel.element.querySelector('.mtrl-carousel__show-all');
625
+ expect(showAllLink).toBeDefined();
626
+ });
627
+
628
+ test('should hide "Show All" link when specified', () => {
629
+ const carousel = createMockCarousel({ showAllLink: false });
630
+ const showAllLink = carousel.element.querySelector('.mtrl-carousel__show-all');
631
+ expect(showAllLink).toBeNull();
632
+ });
633
+
634
+ test('should set initial slide', () => {
635
+ const slides: CarouselSlide[] = [
636
+ { image: 'image1.jpg' },
637
+ { image: 'image2.jpg' },
638
+ { image: 'image3.jpg' }
639
+ ];
640
+
641
+ const carousel = createMockCarousel({ slides, initialSlide: 1 });
642
+
643
+ expect(carousel.getCurrentSlide()).toBe(1);
644
+
645
+ const activeIndicator = carousel.element.querySelector('.mtrl-carousel__indicator--active');
646
+ expect(activeIndicator).toBeDefined();
647
+ expect(activeIndicator?.getAttribute('data-index')).toBe('1');
648
+ });
649
+
650
+ test('should navigate to next slide', () => {
651
+ const slides: CarouselSlide[] = [
652
+ { image: 'image1.jpg' },
653
+ { image: 'image2.jpg' },
654
+ { image: 'image3.jpg' }
655
+ ];
656
+
657
+ const carousel = createMockCarousel({ slides });
658
+
659
+ expect(carousel.getCurrentSlide()).toBe(0);
660
+
661
+ carousel.next();
662
+ expect(carousel.getCurrentSlide()).toBe(1);
663
+
664
+ carousel.next();
665
+ expect(carousel.getCurrentSlide()).toBe(2);
666
+ });
667
+
668
+ test('should navigate to previous slide', () => {
669
+ const slides: CarouselSlide[] = [
670
+ { image: 'image1.jpg' },
671
+ { image: 'image2.jpg' },
672
+ { image: 'image3.jpg' }
673
+ ];
674
+
675
+ const carousel = createMockCarousel({ slides, initialSlide: 2 });
676
+
677
+ expect(carousel.getCurrentSlide()).toBe(2);
678
+
679
+ carousel.prev();
680
+ expect(carousel.getCurrentSlide()).toBe(1);
681
+
682
+ carousel.prev();
683
+ expect(carousel.getCurrentSlide()).toBe(0);
684
+ });
685
+
686
+ test('should loop to first slide when reaching the end', () => {
687
+ const slides: CarouselSlide[] = [
688
+ { image: 'image1.jpg' },
689
+ { image: 'image2.jpg' },
690
+ { image: 'image3.jpg' }
691
+ ];
692
+
693
+ const carousel = createMockCarousel({ slides, loop: true });
694
+
695
+ expect(carousel.getCurrentSlide()).toBe(0);
696
+
697
+ carousel.next();
698
+ expect(carousel.getCurrentSlide()).toBe(1);
699
+
700
+ carousel.next();
701
+ expect(carousel.getCurrentSlide()).toBe(2);
702
+
703
+ carousel.next();
704
+ expect(carousel.getCurrentSlide()).toBe(0);
705
+ });
706
+
707
+ test('should loop to last slide when going back from first', () => {
708
+ const slides: CarouselSlide[] = [
709
+ { image: 'image1.jpg' },
710
+ { image: 'image2.jpg' },
711
+ { image: 'image3.jpg' }
712
+ ];
713
+
714
+ const carousel = createMockCarousel({ slides, loop: true });
715
+
716
+ expect(carousel.getCurrentSlide()).toBe(0);
717
+
718
+ carousel.prev();
719
+ expect(carousel.getCurrentSlide()).toBe(2);
720
+ });
721
+
722
+ test('should not loop when loop is disabled', () => {
723
+ const slides: CarouselSlide[] = [
724
+ { image: 'image1.jpg' },
725
+ { image: 'image2.jpg' },
726
+ { image: 'image3.jpg' }
727
+ ];
728
+
729
+ const carousel = createMockCarousel({ slides, loop: false });
730
+
731
+ expect(carousel.getCurrentSlide()).toBe(0);
732
+
733
+ carousel.prev();
734
+ expect(carousel.getCurrentSlide()).toBe(0); // Stays at first slide
735
+
736
+ carousel.next();
737
+ carousel.next();
738
+ expect(carousel.getCurrentSlide()).toBe(2);
739
+
740
+ carousel.next();
741
+ expect(carousel.getCurrentSlide()).toBe(2); // Stays at last slide
742
+ });
743
+
744
+ test('should go to specific slide', () => {
745
+ const slides: CarouselSlide[] = [
746
+ { image: 'image1.jpg' },
747
+ { image: 'image2.jpg' },
748
+ { image: 'image3.jpg' }
749
+ ];
750
+
751
+ const carousel = createMockCarousel({ slides });
752
+
753
+ expect(carousel.getCurrentSlide()).toBe(0);
754
+
755
+ carousel.goTo(2);
756
+ expect(carousel.getCurrentSlide()).toBe(2);
757
+
758
+ carousel.goTo(1);
759
+ expect(carousel.getCurrentSlide()).toBe(1);
760
+
761
+ // Should ignore invalid indices
762
+ carousel.goTo(-1);
763
+ expect(carousel.getCurrentSlide()).toBe(1); // Remains unchanged
764
+
765
+ carousel.goTo(10);
766
+ expect(carousel.getCurrentSlide()).toBe(1); // Remains unchanged
767
+ });
768
+
769
+ test('should add a new slide', () => {
770
+ const slides: CarouselSlide[] = [
771
+ { image: 'image1.jpg' },
772
+ { image: 'image2.jpg' }
773
+ ];
774
+
775
+ const carousel = createMockCarousel({ slides });
776
+
777
+ expect(carousel.slides.getCount()).toBe(2);
778
+
779
+ const newSlide: CarouselSlide = { image: 'image3.jpg', title: 'New Slide' };
780
+ carousel.addSlide(newSlide);
781
+
782
+ expect(carousel.slides.getCount()).toBe(3);
783
+ expect(carousel.slides.getSlide(2)).toEqual(newSlide);
784
+
785
+ const slideElements = carousel.slides.getElements();
786
+ expect(slideElements.length).toBe(3);
787
+
788
+ const indicators = carousel.element.querySelectorAll('.mtrl-carousel__indicator');
789
+ expect(indicators.length).toBe(3);
790
+ });
791
+
792
+ test('should add a slide at specific index', () => {
793
+ const slides: CarouselSlide[] = [
794
+ { image: 'image1.jpg' },
795
+ { image: 'image3.jpg' }
796
+ ];
797
+
798
+ const carousel = createMockCarousel({ slides });
799
+
800
+ const newSlide: CarouselSlide = { image: 'image2.jpg', title: 'Middle Slide' };
801
+ carousel.addSlide(newSlide, 1);
802
+
803
+ expect(carousel.slides.getCount()).toBe(3);
804
+ expect(carousel.slides.getSlide(1)).toEqual(newSlide);
805
+ });
806
+
807
+ test('should remove a slide', () => {
808
+ // Store the original slides separately for reference
809
+ const originalSlides = [
810
+ { image: 'image1.jpg' },
811
+ { image: 'image2.jpg' },
812
+ { image: 'image3.jpg' }
813
+ ];
814
+
815
+ // Create a copy for the carousel
816
+ const slides = [...originalSlides];
817
+
818
+ const carousel = createMockCarousel({ slides });
819
+
820
+ expect(carousel.slides.getCount()).toBe(3);
821
+
822
+ carousel.removeSlide(1);
823
+
824
+ expect(carousel.slides.getCount()).toBe(2);
825
+
826
+ // Check that the first and third slides from the original array remain
827
+ expect(carousel.slides.getSlide(0)?.image).toBe('image1.jpg');
828
+ expect(carousel.slides.getSlide(1)?.image).toBe('image3.jpg');
829
+
830
+ const slideElements = carousel.slides.getElements();
831
+ expect(slideElements.length).toBe(2);
832
+
833
+ const indicators = carousel.element.querySelectorAll('.mtrl-carousel__indicator');
834
+ expect(indicators.length).toBe(2);
835
+ });
836
+
837
+ test('should update a slide', () => {
838
+ const slides: CarouselSlide[] = [
839
+ { image: 'image1.jpg', title: 'Original Title' },
840
+ { image: 'image2.jpg' }
841
+ ];
842
+
843
+ const carousel = createMockCarousel({ slides });
844
+
845
+ const updatedSlide: CarouselSlide = {
846
+ title: 'Updated Title',
847
+ accent: '#FF0000'
848
+ };
849
+
850
+ carousel.slides.updateSlide(0, updatedSlide);
851
+
852
+ const updatedSlideData = carousel.slides.getSlide(0);
853
+ expect(updatedSlideData?.image).toBe('image1.jpg'); // Original property
854
+ expect(updatedSlideData?.title).toBe('Updated Title'); // Updated property
855
+ expect(updatedSlideData?.accent).toBe('#FF0000'); // New property
856
+
857
+ const slideElement = carousel.slides.getElements()[0];
858
+ const title = slideElement.querySelector('.mtrl-carousel__title');
859
+ expect(title?.textContent).toBe('Updated Title');
860
+ });
861
+
862
+ test('should enable and disable loop mode', () => {
863
+ const slides: CarouselSlide[] = [
864
+ { image: 'image1.jpg' },
865
+ { image: 'image2.jpg' }
866
+ ];
867
+
868
+ const carousel = createMockCarousel({ slides, loop: false });
869
+
870
+ // With loop disabled, we should stay at the first slide
871
+ carousel.prev();
872
+ expect(carousel.getCurrentSlide()).toBe(0);
873
+
874
+ // Enable loop
875
+ carousel.enableLoop();
876
+
877
+ // Now we should loop to the last slide
878
+ carousel.prev();
879
+ expect(carousel.getCurrentSlide()).toBe(1);
880
+
881
+ // Go back to the first slide
882
+ carousel.next();
883
+ expect(carousel.getCurrentSlide()).toBe(0);
884
+
885
+ // Disable loop
886
+ carousel.disableLoop();
887
+
888
+ // Move to last slide
889
+ carousel.next();
890
+ expect(carousel.getCurrentSlide()).toBe(1);
891
+
892
+ // With loop disabled, we should stay at the last slide
893
+ carousel.next();
894
+ expect(carousel.getCurrentSlide()).toBe(1);
895
+ });
896
+
897
+ test('should set border radius', () => {
898
+ const carousel = createMockCarousel();
899
+
900
+ carousel.setBorderRadius(24);
901
+ expect(carousel.element.style.getPropertyValue('--carousel-border-radius')).toBe('24px');
902
+ });
903
+
904
+ test('should set gap between slides', () => {
905
+ const carousel = createMockCarousel();
906
+
907
+ carousel.setGap(16);
908
+ expect(carousel.element.style.getPropertyValue('--carousel-gap')).toBe('16px');
909
+ });
910
+
911
+ test('should add event listener', () => {
912
+ const carousel = createMockCarousel();
913
+ let eventFired = false;
914
+
915
+ carousel.on('slideChange', () => {
916
+ eventFired = true;
917
+ });
918
+
919
+ carousel.next();
920
+ expect(eventFired).toBe(true);
921
+ });
922
+
923
+ test('should remove event listener', () => {
924
+ const carousel = createMockCarousel();
925
+ let eventCount = 0;
926
+
927
+ const handler = () => {
928
+ eventCount++;
929
+ };
930
+
931
+ carousel.on('slideChange', handler);
932
+
933
+ carousel.next();
934
+ expect(eventCount).toBe(1);
935
+
936
+ carousel.off('slideChange', handler);
937
+
938
+ carousel.next();
939
+ expect(eventCount).toBe(1); // Counter should not increase
940
+ });
941
+
942
+ test('should be properly destroyed', () => {
943
+ const carousel = createMockCarousel();
944
+ document.body.appendChild(carousel.element);
945
+
946
+ expect(document.body.contains(carousel.element)).toBe(true);
947
+
948
+ carousel.destroy();
949
+ expect(document.body.contains(carousel.element)).toBe(false);
950
+ });
951
+ });