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,990 @@
1
+ // test/components/dialog.test.ts
2
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
3
+ import { JSDOM } from 'jsdom';
4
+ import {
5
+ type DialogComponent,
6
+ type DialogConfig,
7
+ type DialogButton,
8
+ type DialogSize,
9
+ type DialogAnimation,
10
+ type DialogFooterAlignment,
11
+ type DialogEventType
12
+ } from '../../src/components/dialog/types';
13
+
14
+ // Setup jsdom environment
15
+ let dom: JSDOM;
16
+ let window: Window;
17
+ let document: Document;
18
+ let originalGlobalDocument: any;
19
+ let originalGlobalWindow: any;
20
+
21
+ beforeAll(() => {
22
+ // Create a new JSDOM instance
23
+ dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
24
+ url: 'http://localhost/',
25
+ pretendToBeVisual: true
26
+ });
27
+
28
+ // Get window and document from jsdom
29
+ window = dom.window;
30
+ document = window.document;
31
+
32
+ // Store original globals
33
+ originalGlobalDocument = global.document;
34
+ originalGlobalWindow = global.window;
35
+
36
+ // Set globals to use jsdom
37
+ global.document = document;
38
+ global.window = window;
39
+ global.Element = window.Element;
40
+ global.HTMLElement = window.HTMLElement;
41
+ global.HTMLButtonElement = window.HTMLButtonElement;
42
+ global.Event = window.Event;
43
+ });
44
+
45
+ afterAll(() => {
46
+ // Restore original globals
47
+ global.document = originalGlobalDocument;
48
+ global.window = originalGlobalWindow;
49
+
50
+ // Clean up jsdom
51
+ window.close();
52
+ });
53
+
54
+ // Mock dialog implementation
55
+ const createMockDialog = (config: DialogConfig = {}): DialogComponent => {
56
+ // Create main elements
57
+ const element = document.createElement('div');
58
+ element.className = 'mtrl-dialog';
59
+
60
+ const overlay = document.createElement('div');
61
+ overlay.className = 'mtrl-dialog-overlay';
62
+
63
+ // Default settings
64
+ const settings = {
65
+ title: config.title || '',
66
+ subtitle: config.subtitle || '',
67
+ content: config.content || '',
68
+ closeButton: config.closeButton !== undefined ? config.closeButton : true,
69
+ size: config.size || 'medium',
70
+ animation: config.animation || 'scale',
71
+ footerAlignment: config.footerAlignment || 'right',
72
+ closeOnOverlayClick: config.closeOnOverlayClick !== undefined ? config.closeOnOverlayClick : true,
73
+ closeOnEscape: config.closeOnEscape !== undefined ? config.closeOnEscape : true,
74
+ modal: config.modal !== undefined ? config.modal : true,
75
+ autofocus: config.autofocus !== undefined ? config.autofocus : true,
76
+ trapFocus: config.trapFocus !== undefined ? config.trapFocus : true,
77
+ divider: config.divider !== undefined ? config.divider : false,
78
+ zIndex: config.zIndex || 1000,
79
+ animationDuration: config.animationDuration || 300,
80
+ buttons: [...(config.buttons || [])]
81
+ };
82
+
83
+ // Set size class
84
+ if (settings.size) {
85
+ element.classList.add(`mtrl-dialog--${settings.size}`);
86
+ }
87
+
88
+ // Set animation class
89
+ if (settings.animation) {
90
+ element.classList.add(`mtrl-dialog--animation-${settings.animation}`);
91
+ }
92
+
93
+ // Create dialog structure
94
+ const container = document.createElement('div');
95
+ container.className = 'mtrl-dialog-container';
96
+
97
+ // Create dialog header
98
+ const header = document.createElement('div');
99
+ header.className = 'mtrl-dialog-header';
100
+
101
+ const titleElement = document.createElement('h2');
102
+ titleElement.className = 'mtrl-dialog-title';
103
+ titleElement.textContent = settings.title;
104
+ header.appendChild(titleElement);
105
+
106
+ if (settings.subtitle) {
107
+ const subtitleElement = document.createElement('h3');
108
+ subtitleElement.className = 'mtrl-dialog-subtitle';
109
+ subtitleElement.textContent = settings.subtitle;
110
+ header.appendChild(subtitleElement);
111
+ }
112
+
113
+ if (settings.closeButton) {
114
+ const closeButton = document.createElement('button');
115
+ closeButton.className = 'mtrl-dialog-close';
116
+ closeButton.setAttribute('aria-label', 'Close dialog');
117
+ closeButton.innerHTML = '&times;';
118
+ closeButton.addEventListener('click', () => {
119
+ dialog.close();
120
+ });
121
+ header.appendChild(closeButton);
122
+ }
123
+
124
+ container.appendChild(header);
125
+
126
+ // Add divider if configured
127
+ if (settings.divider) {
128
+ const divider = document.createElement('hr');
129
+ divider.className = 'mtrl-dialog-divider';
130
+ container.appendChild(divider);
131
+ }
132
+
133
+ // Create dialog content
134
+ const content = document.createElement('div');
135
+ content.className = 'mtrl-dialog-content';
136
+
137
+ if (settings.content) {
138
+ if (settings.content.includes('<')) {
139
+ content.innerHTML = settings.content;
140
+ } else {
141
+ content.textContent = settings.content;
142
+ }
143
+ }
144
+
145
+ container.appendChild(content);
146
+
147
+ // Create dialog footer with buttons
148
+ const footer = document.createElement('div');
149
+ footer.className = 'mtrl-dialog-footer';
150
+ footer.classList.add(`mtrl-dialog-footer--${settings.footerAlignment}`);
151
+
152
+ const buttons: DialogButton[] = [...settings.buttons];
153
+
154
+ buttons.forEach(button => {
155
+ const buttonElement = document.createElement('button');
156
+ buttonElement.className = 'mtrl-button';
157
+
158
+ if (button.variant) {
159
+ buttonElement.classList.add(`mtrl-button--${button.variant}`);
160
+ }
161
+
162
+ if (button.color) {
163
+ buttonElement.classList.add(`mtrl-button--${button.color}`);
164
+ }
165
+
166
+ if (button.size) {
167
+ buttonElement.classList.add(`mtrl-button--${button.size}`);
168
+ }
169
+
170
+ buttonElement.textContent = button.text;
171
+
172
+ if (button.attrs) {
173
+ for (const [key, value] of Object.entries(button.attrs)) {
174
+ buttonElement.setAttribute(key, value);
175
+ }
176
+ }
177
+
178
+ if (button.autofocus) {
179
+ buttonElement.setAttribute('autofocus', 'true');
180
+ }
181
+
182
+ buttonElement.addEventListener('click', (event) => {
183
+ let shouldClose = button.closeDialog !== false;
184
+
185
+ if (button.onClick) {
186
+ const result = button.onClick(event, dialog);
187
+ if (result === false) {
188
+ shouldClose = false;
189
+ }
190
+ }
191
+
192
+ if (shouldClose) {
193
+ dialog.close();
194
+ }
195
+ });
196
+
197
+ footer.appendChild(buttonElement);
198
+ });
199
+
200
+ container.appendChild(footer);
201
+ element.appendChild(container);
202
+
203
+ // Track open state
204
+ let isDialogOpen = config.open || false;
205
+
206
+ // Track event handlers
207
+ const eventHandlers: Record<string, Function[]> = {};
208
+
209
+ // Initialize event handlers from config
210
+ if (config.on) {
211
+ for (const [event, handler] of Object.entries(config.on)) {
212
+ if (!eventHandlers[event]) {
213
+ eventHandlers[event] = [];
214
+ }
215
+ eventHandlers[event].push(handler);
216
+ }
217
+ }
218
+
219
+ // Create dialog event
220
+ const createDialogEvent = (type: string, originalEvent?: Event): any => {
221
+ let defaultPrevented = false;
222
+
223
+ return {
224
+ dialog,
225
+ originalEvent,
226
+ preventDefault: () => {
227
+ defaultPrevented = true;
228
+ },
229
+ defaultPrevented
230
+ };
231
+ };
232
+
233
+ // Emit event
234
+ const emit = (eventType: string, originalEvent?: Event) => {
235
+ if (eventHandlers[eventType]) {
236
+ const event = createDialogEvent(eventType, originalEvent);
237
+ for (const handler of eventHandlers[eventType]) {
238
+ handler(event);
239
+ }
240
+ return event.defaultPrevented;
241
+ }
242
+ return false;
243
+ };
244
+
245
+ // Set Z-index
246
+ overlay.style.zIndex = String(settings.zIndex);
247
+ element.style.zIndex = String(settings.zIndex + 1);
248
+
249
+ // Add class
250
+ if (config.class) {
251
+ const classes = config.class.split(' ');
252
+ classes.forEach(cls => element.classList.add(cls));
253
+ }
254
+
255
+ // Initial setup
256
+ if (isDialogOpen) {
257
+ overlay.classList.add('mtrl-dialog-overlay--open');
258
+ element.classList.add('mtrl-dialog--open');
259
+ } else {
260
+ overlay.style.display = 'none';
261
+ element.style.display = 'none';
262
+ }
263
+
264
+ // Handle overlay click
265
+ overlay.addEventListener('click', (event) => {
266
+ if (settings.closeOnOverlayClick && event.target === overlay) {
267
+ dialog.close();
268
+ }
269
+ });
270
+
271
+ // Handle escape key
272
+ const handleEscapeKey = (event: any) => {
273
+ if (settings.closeOnEscape && isDialogOpen && event.key === 'Escape') {
274
+ dialog.close();
275
+ }
276
+ };
277
+
278
+ document.addEventListener('keydown', handleEscapeKey);
279
+
280
+ // Create dialog component
281
+ const dialog: DialogComponent = {
282
+ element,
283
+ overlay,
284
+
285
+ open: () => {
286
+ if (!isDialogOpen) {
287
+ // Emit beforeopen event
288
+ if (emit('beforeopen')) {
289
+ return dialog; // Event was prevented
290
+ }
291
+
292
+ overlay.style.display = 'block';
293
+ element.style.display = 'block';
294
+
295
+ // For tests, we run immediately instead of setTimeout
296
+ overlay.classList.add('mtrl-dialog-overlay--open');
297
+ element.classList.add('mtrl-dialog--open');
298
+
299
+ isDialogOpen = true;
300
+
301
+ // Auto focus the first button or focusable element
302
+ if (settings.autofocus) {
303
+ const autofocusButton = element.querySelector('[autofocus]');
304
+ if (autofocusButton) {
305
+ (autofocusButton as HTMLElement).focus();
306
+ } else {
307
+ const firstFocusable = element.querySelector('button, [tabindex], a, input, select, textarea');
308
+ if (firstFocusable) {
309
+ (firstFocusable as HTMLElement).focus();
310
+ }
311
+ }
312
+ }
313
+
314
+ // Emit afteropen event
315
+ emit('afteropen');
316
+
317
+ // Emit open event
318
+ emit('open');
319
+ }
320
+
321
+ return dialog;
322
+ },
323
+
324
+ close: () => {
325
+ if (isDialogOpen) {
326
+ // Emit beforeclose event
327
+ if (emit('beforeclose')) {
328
+ return dialog; // Event was prevented
329
+ }
330
+
331
+ overlay.classList.remove('mtrl-dialog-overlay--open');
332
+ element.classList.remove('mtrl-dialog--open');
333
+
334
+ // For tests, we run immediately instead of setTimeout
335
+ if (!isDialogOpen) {
336
+ overlay.style.display = 'none';
337
+ element.style.display = 'none';
338
+ }
339
+
340
+ // Emit afterclose event
341
+ emit('afterclose');
342
+
343
+ isDialogOpen = false;
344
+
345
+ // Emit close event
346
+ emit('close');
347
+ }
348
+
349
+ return dialog;
350
+ },
351
+
352
+ toggle: (open?: boolean) => {
353
+ if (open === undefined) {
354
+ return isDialogOpen ? dialog.close() : dialog.open();
355
+ } else {
356
+ return open ? dialog.open() : dialog.close();
357
+ }
358
+ },
359
+
360
+ isOpen: () => isDialogOpen,
361
+
362
+ setTitle: (title: string) => {
363
+ settings.title = title;
364
+ const titleEl = element.querySelector('.mtrl-dialog-title');
365
+ if (titleEl) {
366
+ titleEl.textContent = title;
367
+ }
368
+ return dialog;
369
+ },
370
+
371
+ getTitle: () => settings.title,
372
+
373
+ setSubtitle: (subtitle: string) => {
374
+ settings.subtitle = subtitle;
375
+
376
+ let subtitleEl = element.querySelector('.mtrl-dialog-subtitle');
377
+ if (!subtitleEl && subtitle) {
378
+ // Create subtitle element if it doesn't exist
379
+ subtitleEl = document.createElement('h3');
380
+ subtitleEl.className = 'mtrl-dialog-subtitle';
381
+ const titleEl = element.querySelector('.mtrl-dialog-title');
382
+ if (titleEl?.parentNode) {
383
+ titleEl.parentNode.insertBefore(subtitleEl, titleEl.nextSibling);
384
+ }
385
+ }
386
+
387
+ if (subtitleEl) {
388
+ if (subtitle) {
389
+ subtitleEl.textContent = subtitle;
390
+ } else {
391
+ subtitleEl.parentNode?.removeChild(subtitleEl);
392
+ }
393
+ }
394
+
395
+ return dialog;
396
+ },
397
+
398
+ getSubtitle: () => settings.subtitle,
399
+
400
+ setContent: (content: string) => {
401
+ settings.content = content;
402
+
403
+ const contentEl = element.querySelector('.mtrl-dialog-content');
404
+ if (contentEl) {
405
+ if (content.includes('<')) {
406
+ contentEl.innerHTML = content;
407
+ } else {
408
+ contentEl.textContent = content;
409
+ }
410
+ }
411
+
412
+ return dialog;
413
+ },
414
+
415
+ getContent: () => settings.content,
416
+
417
+ addButton: (button: DialogButton) => {
418
+ settings.buttons.push(button);
419
+
420
+ const footerEl = element.querySelector('.mtrl-dialog-footer');
421
+ if (footerEl) {
422
+ const buttonElement = document.createElement('button');
423
+ buttonElement.className = 'mtrl-button';
424
+
425
+ if (button.variant) {
426
+ buttonElement.classList.add(`mtrl-button--${button.variant}`);
427
+ }
428
+
429
+ if (button.color) {
430
+ buttonElement.classList.add(`mtrl-button--${button.color}`);
431
+ }
432
+
433
+ if (button.size) {
434
+ buttonElement.classList.add(`mtrl-button--${button.size}`);
435
+ }
436
+
437
+ buttonElement.textContent = button.text;
438
+
439
+ if (button.attrs) {
440
+ for (const [key, value] of Object.entries(button.attrs)) {
441
+ buttonElement.setAttribute(key, value);
442
+ }
443
+ }
444
+
445
+ if (button.autofocus) {
446
+ buttonElement.setAttribute('autofocus', 'true');
447
+ }
448
+
449
+ buttonElement.addEventListener('click', (event) => {
450
+ let shouldClose = button.closeDialog !== false;
451
+
452
+ if (button.onClick) {
453
+ const result = button.onClick(event, dialog);
454
+ if (result === false) {
455
+ shouldClose = false;
456
+ }
457
+ }
458
+
459
+ if (shouldClose) {
460
+ dialog.close();
461
+ }
462
+ });
463
+
464
+ footerEl.appendChild(buttonElement);
465
+ }
466
+
467
+ return dialog;
468
+ },
469
+
470
+ removeButton: (indexOrText: number | string) => {
471
+ const footerEl = element.querySelector('.mtrl-dialog-footer');
472
+ if (footerEl) {
473
+ if (typeof indexOrText === 'number') {
474
+ if (indexOrText >= 0 && indexOrText < settings.buttons.length) {
475
+ settings.buttons.splice(indexOrText, 1);
476
+ const buttons = footerEl.querySelectorAll('.mtrl-button');
477
+ if (indexOrText < buttons.length) {
478
+ footerEl.removeChild(buttons[indexOrText]);
479
+ }
480
+ }
481
+ } else {
482
+ const buttonIndex = settings.buttons.findIndex(b => b.text === indexOrText);
483
+ if (buttonIndex !== -1) {
484
+ settings.buttons.splice(buttonIndex, 1);
485
+ const buttons = footerEl.querySelectorAll('.mtrl-button');
486
+ if (buttonIndex < buttons.length) {
487
+ footerEl.removeChild(buttons[buttonIndex]);
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ return dialog;
494
+ },
495
+
496
+ getButtons: () => [...settings.buttons],
497
+
498
+ setFooterAlignment: (alignment: DialogFooterAlignment | string) => {
499
+ settings.footerAlignment = alignment;
500
+
501
+ const footerEl = element.querySelector('.mtrl-dialog-footer');
502
+ if (footerEl) {
503
+ footerEl.className = 'mtrl-dialog-footer';
504
+ footerEl.classList.add(`mtrl-dialog-footer--${alignment}`);
505
+ }
506
+
507
+ return dialog;
508
+ },
509
+
510
+ setSize: (size: DialogSize | string) => {
511
+ const prevSize = settings.size;
512
+ settings.size = size;
513
+
514
+ if (prevSize) {
515
+ element.classList.remove(`mtrl-dialog--${prevSize}`);
516
+ }
517
+
518
+ element.classList.add(`mtrl-dialog--${size}`);
519
+
520
+ return dialog;
521
+ },
522
+
523
+ on: (event: DialogEventType | string, handler: Function) => {
524
+ if (!eventHandlers[event]) {
525
+ eventHandlers[event] = [];
526
+ }
527
+
528
+ eventHandlers[event].push(handler);
529
+ return dialog;
530
+ },
531
+
532
+ off: (event: DialogEventType | string, handler: Function) => {
533
+ if (eventHandlers[event]) {
534
+ eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
535
+ }
536
+
537
+ return dialog;
538
+ },
539
+
540
+ getHeaderElement: () => element.querySelector('.mtrl-dialog-header'),
541
+
542
+ getContentElement: () => element.querySelector('.mtrl-dialog-content'),
543
+
544
+ getFooterElement: () => element.querySelector('.mtrl-dialog-footer'),
545
+
546
+ toggleDivider: (show: boolean) => {
547
+ settings.divider = show;
548
+
549
+ let dividerEl = element.querySelector('.mtrl-dialog-divider');
550
+ if (show && !dividerEl) {
551
+ dividerEl = document.createElement('hr');
552
+ dividerEl.className = 'mtrl-dialog-divider';
553
+
554
+ const headerEl = element.querySelector('.mtrl-dialog-header');
555
+ const contentEl = element.querySelector('.mtrl-dialog-content');
556
+
557
+ if (headerEl && contentEl && headerEl.parentNode) {
558
+ headerEl.parentNode.insertBefore(dividerEl, contentEl);
559
+ }
560
+ } else if (!show && dividerEl) {
561
+ dividerEl.parentNode?.removeChild(dividerEl);
562
+ }
563
+
564
+ return dialog;
565
+ },
566
+
567
+ hasDivider: () => settings.divider,
568
+
569
+ confirm: (options?: any) => {
570
+ // Simple mock implementation of confirm
571
+ return new Promise<boolean>(resolve => {
572
+ const confirmTitle = options?.title || 'Confirm';
573
+ const confirmMessage = options?.message || 'Are you sure?';
574
+ const confirmText = options?.confirmText || 'Yes';
575
+ const cancelText = options?.cancelText || 'No';
576
+
577
+ // In real implementation this would create a new dialog
578
+ // For testing purposes we just resolve immediately
579
+ const willConfirm = true; // simulate confirmation
580
+ resolve(willConfirm);
581
+ });
582
+ },
583
+
584
+ destroy: () => {
585
+ // Clean up event listeners
586
+ document.removeEventListener('keydown', handleEscapeKey);
587
+
588
+ // Remove from the DOM
589
+ if (overlay.parentNode) {
590
+ overlay.parentNode.removeChild(overlay);
591
+ }
592
+
593
+ if (element.parentNode) {
594
+ element.parentNode.removeChild(element);
595
+ }
596
+
597
+ // Clear event handlers
598
+ for (const event in eventHandlers) {
599
+ eventHandlers[event] = [];
600
+ }
601
+ }
602
+ };
603
+
604
+ return dialog;
605
+ };
606
+
607
+ describe('Dialog Component', () => {
608
+ test('should create a dialog element', () => {
609
+ const dialog = createMockDialog();
610
+
611
+ expect(dialog.element).toBeDefined();
612
+ expect(dialog.element.tagName).toBe('DIV');
613
+ expect(dialog.element.className).toContain('mtrl-dialog');
614
+
615
+ expect(dialog.overlay).toBeDefined();
616
+ expect(dialog.overlay.tagName).toBe('DIV');
617
+ expect(dialog.overlay.className).toContain('mtrl-dialog-overlay');
618
+ });
619
+
620
+ test('should set title and subtitle', () => {
621
+ const dialog = createMockDialog({
622
+ title: 'Dialog Title',
623
+ subtitle: 'Dialog Subtitle'
624
+ });
625
+
626
+ const titleElement = dialog.element.querySelector('.mtrl-dialog-title');
627
+ expect(titleElement).toBeDefined();
628
+ expect(titleElement?.textContent).toBe('Dialog Title');
629
+
630
+ const subtitleElement = dialog.element.querySelector('.mtrl-dialog-subtitle');
631
+ expect(subtitleElement).toBeDefined();
632
+ expect(subtitleElement?.textContent).toBe('Dialog Subtitle');
633
+
634
+ expect(dialog.getTitle()).toBe('Dialog Title');
635
+ expect(dialog.getSubtitle()).toBe('Dialog Subtitle');
636
+ });
637
+
638
+ test('should set content', () => {
639
+ const dialog = createMockDialog({
640
+ content: 'Dialog content text'
641
+ });
642
+
643
+ const contentElement = dialog.element.querySelector('.mtrl-dialog-content');
644
+ expect(contentElement).toBeDefined();
645
+ expect(contentElement?.textContent).toBe('Dialog content text');
646
+
647
+ expect(dialog.getContent()).toBe('Dialog content text');
648
+ });
649
+
650
+ test('should support HTML content', () => {
651
+ const dialog = createMockDialog({
652
+ content: '<p>Dialog <strong>HTML</strong> content</p>'
653
+ });
654
+
655
+ const contentElement = dialog.element.querySelector('.mtrl-dialog-content');
656
+ expect(contentElement).toBeDefined();
657
+ expect(contentElement?.innerHTML).toBe('<p>Dialog <strong>HTML</strong> content</p>');
658
+
659
+ const strongElement = contentElement?.querySelector('strong');
660
+ expect(strongElement).toBeDefined();
661
+ expect(strongElement?.textContent).toBe('HTML');
662
+ });
663
+
664
+ test('should apply size variants', () => {
665
+ const sizes: DialogSize[] = ['small', 'medium', 'large', 'fullwidth', 'fullscreen'];
666
+
667
+ sizes.forEach(size => {
668
+ const dialog = createMockDialog({ size });
669
+ expect(dialog.element.className).toContain(`mtrl-dialog--${size}`);
670
+ });
671
+ });
672
+
673
+ test('should apply animation variants', () => {
674
+ const animations: DialogAnimation[] = ['scale', 'slide-up', 'slide-down', 'fade'];
675
+
676
+ animations.forEach(animation => {
677
+ const dialog = createMockDialog({ animation });
678
+ expect(dialog.element.className).toContain(`mtrl-dialog--animation-${animation}`);
679
+ });
680
+ });
681
+
682
+ test('should create close button by default', () => {
683
+ const dialog = createMockDialog();
684
+
685
+ const closeButton = dialog.element.querySelector('.mtrl-dialog-close');
686
+ expect(closeButton).toBeDefined();
687
+ });
688
+
689
+ test('should hide close button when configured', () => {
690
+ const dialog = createMockDialog({
691
+ closeButton: false
692
+ });
693
+
694
+ const closeButton = dialog.element.querySelector('.mtrl-dialog-close');
695
+ expect(closeButton).toBeNull();
696
+ });
697
+
698
+ test('should create buttons in footer', () => {
699
+ const buttons: DialogButton[] = [
700
+ { text: 'Cancel', variant: 'text' },
701
+ { text: 'OK', variant: 'filled' }
702
+ ];
703
+
704
+ const dialog = createMockDialog({ buttons });
705
+
706
+ const footerElement = dialog.element.querySelector('.mtrl-dialog-footer');
707
+ expect(footerElement).toBeDefined();
708
+
709
+ const buttonElements = dialog.element.querySelectorAll('.mtrl-button');
710
+ expect(buttonElements.length).toBe(2);
711
+
712
+ expect(buttonElements[0].textContent).toBe('Cancel');
713
+ expect(buttonElements[0].className).toContain('mtrl-button--text');
714
+
715
+ expect(buttonElements[1].textContent).toBe('OK');
716
+ expect(buttonElements[1].className).toContain('mtrl-button--filled');
717
+ });
718
+
719
+ test('should initially be closed by default', () => {
720
+ const dialog = createMockDialog();
721
+
722
+ expect(dialog.isOpen()).toBe(false);
723
+ expect(dialog.element.style.display).toBe('none');
724
+ expect(dialog.overlay.style.display).toBe('none');
725
+ });
726
+
727
+ test('should initially be open when configured', () => {
728
+ const dialog = createMockDialog({
729
+ open: true
730
+ });
731
+
732
+ expect(dialog.isOpen()).toBe(true);
733
+ expect(dialog.element.className).toContain('mtrl-dialog--open');
734
+ expect(dialog.overlay.className).toContain('mtrl-dialog-overlay--open');
735
+ });
736
+
737
+ test('should open and close the dialog', () => {
738
+ const dialog = createMockDialog();
739
+
740
+ expect(dialog.isOpen()).toBe(false);
741
+
742
+ dialog.open();
743
+
744
+ expect(dialog.isOpen()).toBe(true);
745
+ expect(dialog.element.style.display).toBe('block');
746
+ expect(dialog.overlay.style.display).toBe('block');
747
+ expect(dialog.element.className).toContain('mtrl-dialog--open');
748
+
749
+ dialog.close();
750
+
751
+ expect(dialog.isOpen()).toBe(false);
752
+ expect(dialog.element.className).not.toContain('mtrl-dialog--open');
753
+ });
754
+
755
+ test('should toggle the dialog', () => {
756
+ const dialog = createMockDialog();
757
+
758
+ expect(dialog.isOpen()).toBe(false);
759
+
760
+ dialog.toggle();
761
+ expect(dialog.isOpen()).toBe(true);
762
+
763
+ dialog.toggle();
764
+ expect(dialog.isOpen()).toBe(false);
765
+
766
+ dialog.toggle(true);
767
+ expect(dialog.isOpen()).toBe(true);
768
+
769
+ dialog.toggle(true); // Should remain open
770
+ expect(dialog.isOpen()).toBe(true);
771
+
772
+ dialog.toggle(false);
773
+ expect(dialog.isOpen()).toBe(false);
774
+ });
775
+
776
+ test('should update title dynamically', () => {
777
+ const dialog = createMockDialog({
778
+ title: 'Initial Title'
779
+ });
780
+
781
+ const titleElement = dialog.element.querySelector('.mtrl-dialog-title');
782
+ expect(titleElement?.textContent).toBe('Initial Title');
783
+
784
+ dialog.setTitle('Updated Title');
785
+
786
+ expect(titleElement?.textContent).toBe('Updated Title');
787
+ expect(dialog.getTitle()).toBe('Updated Title');
788
+ });
789
+
790
+ test('should update subtitle dynamically', () => {
791
+ const dialog = createMockDialog({
792
+ subtitle: 'Initial Subtitle'
793
+ });
794
+
795
+ const subtitleElement = dialog.element.querySelector('.mtrl-dialog-subtitle');
796
+ expect(subtitleElement?.textContent).toBe('Initial Subtitle');
797
+
798
+ dialog.setSubtitle('Updated Subtitle');
799
+
800
+ expect(subtitleElement?.textContent).toBe('Updated Subtitle');
801
+ expect(dialog.getSubtitle()).toBe('Updated Subtitle');
802
+ });
803
+
804
+ test('should update content dynamically', () => {
805
+ const dialog = createMockDialog({
806
+ content: 'Initial content'
807
+ });
808
+
809
+ const contentElement = dialog.element.querySelector('.mtrl-dialog-content');
810
+ expect(contentElement?.textContent).toBe('Initial content');
811
+
812
+ dialog.setContent('Updated content');
813
+
814
+ expect(contentElement?.textContent).toBe('Updated content');
815
+ expect(dialog.getContent()).toBe('Updated content');
816
+ });
817
+
818
+ test('should add buttons dynamically', () => {
819
+ const dialog = createMockDialog();
820
+
821
+ const footerElement = dialog.element.querySelector('.mtrl-dialog-footer');
822
+ expect(footerElement?.children.length).toBe(0);
823
+
824
+ dialog.addButton({ text: 'New Button', variant: 'text' });
825
+
826
+ expect(footerElement?.children.length).toBe(1);
827
+ expect(footerElement?.children[0].textContent).toBe('New Button');
828
+ expect(dialog.getButtons().length).toBe(1);
829
+ });
830
+
831
+ test('should remove buttons by index', () => {
832
+ const dialog = createMockDialog({
833
+ buttons: [
834
+ { text: 'Button 1' },
835
+ { text: 'Button 2' },
836
+ { text: 'Button 3' }
837
+ ]
838
+ });
839
+
840
+ const footerElement = dialog.element.querySelector('.mtrl-dialog-footer');
841
+ expect(footerElement?.children.length).toBe(3);
842
+
843
+ dialog.removeButton(1);
844
+
845
+ expect(footerElement?.children.length).toBe(2);
846
+ expect(footerElement?.children[0].textContent).toBe('Button 1');
847
+ expect(footerElement?.children[1].textContent).toBe('Button 3');
848
+ expect(dialog.getButtons().length).toBe(2);
849
+ });
850
+
851
+ test('should remove buttons by text', () => {
852
+ const dialog = createMockDialog({
853
+ buttons: [
854
+ { text: 'Button 1' },
855
+ { text: 'Button 2' },
856
+ { text: 'Button 3' }
857
+ ]
858
+ });
859
+
860
+ const footerElement = dialog.element.querySelector('.mtrl-dialog-footer');
861
+ expect(footerElement?.children.length).toBe(3);
862
+
863
+ dialog.removeButton('Button 2');
864
+
865
+ expect(footerElement?.children.length).toBe(2);
866
+ expect(footerElement?.children[0].textContent).toBe('Button 1');
867
+ expect(footerElement?.children[1].textContent).toBe('Button 3');
868
+ expect(dialog.getButtons().length).toBe(2);
869
+ });
870
+
871
+ test('should change footer alignment', () => {
872
+ const dialog = createMockDialog({
873
+ footerAlignment: 'right'
874
+ });
875
+
876
+ const footerElement = dialog.element.querySelector('.mtrl-dialog-footer');
877
+ expect(footerElement?.className).toContain('mtrl-dialog-footer--right');
878
+
879
+ dialog.setFooterAlignment('center');
880
+
881
+ expect(footerElement?.className).toContain('mtrl-dialog-footer--center');
882
+ expect(footerElement?.className).not.toContain('mtrl-dialog-footer--right');
883
+ });
884
+
885
+ test('should change dialog size', () => {
886
+ const dialog = createMockDialog({
887
+ size: 'medium'
888
+ });
889
+
890
+ expect(dialog.element.className).toContain('mtrl-dialog--medium');
891
+
892
+ dialog.setSize('large');
893
+
894
+ expect(dialog.element.className).toContain('mtrl-dialog--large');
895
+ expect(dialog.element.className).not.toContain('mtrl-dialog--medium');
896
+ });
897
+
898
+ test('should add and remove event listeners', () => {
899
+ const dialog = createMockDialog();
900
+ let eventCount = 0;
901
+
902
+ const handler = () => {
903
+ eventCount++;
904
+ };
905
+
906
+ dialog.on('open', handler);
907
+
908
+ dialog.open();
909
+ expect(eventCount).toBe(1);
910
+
911
+ dialog.close();
912
+ dialog.open();
913
+ expect(eventCount).toBe(2);
914
+
915
+ dialog.off('open', handler);
916
+
917
+ dialog.close();
918
+ dialog.open();
919
+ expect(eventCount).toBe(2); // Should not increment
920
+ });
921
+
922
+ test('should get dialog elements', () => {
923
+ const dialog = createMockDialog();
924
+
925
+ const headerElement = dialog.getHeaderElement();
926
+ expect(headerElement).toBeDefined();
927
+ expect(headerElement?.className).toBe('mtrl-dialog-header');
928
+
929
+ const contentElement = dialog.getContentElement();
930
+ expect(contentElement).toBeDefined();
931
+ expect(contentElement?.className).toBe('mtrl-dialog-content');
932
+
933
+ const footerElement = dialog.getFooterElement();
934
+ expect(footerElement).toBeDefined();
935
+ expect(footerElement?.className).toContain('mtrl-dialog-footer');
936
+ });
937
+
938
+ test('should toggle divider', () => {
939
+ const dialog = createMockDialog();
940
+
941
+ expect(dialog.hasDivider()).toBe(false);
942
+ expect(dialog.element.querySelector('.mtrl-dialog-divider')).toBeNull();
943
+
944
+ dialog.toggleDivider(true);
945
+
946
+ expect(dialog.hasDivider()).toBe(true);
947
+ expect(dialog.element.querySelector('.mtrl-dialog-divider')).toBeDefined();
948
+
949
+ dialog.toggleDivider(false);
950
+
951
+ expect(dialog.hasDivider()).toBe(false);
952
+ expect(dialog.element.querySelector('.mtrl-dialog-divider')).toBeNull();
953
+ });
954
+
955
+ test('should create with divider when configured', () => {
956
+ const dialog = createMockDialog({
957
+ divider: true
958
+ });
959
+
960
+ expect(dialog.hasDivider()).toBe(true);
961
+ expect(dialog.element.querySelector('.mtrl-dialog-divider')).toBeDefined();
962
+ });
963
+
964
+ test('should handle confirm method', async () => {
965
+ const dialog = createMockDialog();
966
+
967
+ const result = await dialog.confirm({
968
+ title: 'Confirm Title',
969
+ message: 'Confirm Message',
970
+ confirmText: 'Yes',
971
+ cancelText: 'No'
972
+ });
973
+
974
+ expect(result).toBe(true);
975
+ });
976
+
977
+ test('should be properly destroyed', () => {
978
+ const dialog = createMockDialog();
979
+ document.body.appendChild(dialog.element);
980
+ document.body.appendChild(dialog.overlay);
981
+
982
+ expect(document.body.contains(dialog.element)).toBe(true);
983
+ expect(document.body.contains(dialog.overlay)).toBe(true);
984
+
985
+ dialog.destroy();
986
+
987
+ expect(document.body.contains(dialog.element)).toBe(false);
988
+ expect(document.body.contains(dialog.overlay)).toBe(false);
989
+ });
990
+ });