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.
- package/CLAUDE.md +33 -0
- package/index.ts +0 -2
- package/package.json +3 -1
- package/src/components/navigation/index.ts +4 -1
- package/src/components/navigation/types.ts +33 -0
- package/src/components/snackbar/index.ts +7 -1
- package/src/components/snackbar/types.ts +25 -0
- package/src/components/switch/index.ts +5 -1
- package/src/components/switch/types.ts +13 -0
- package/src/components/textfield/index.ts +7 -1
- package/src/components/textfield/types.ts +36 -0
- package/test/components/badge.test.ts +545 -0
- package/test/components/bottom-app-bar.test.ts +303 -0
- package/test/components/button.test.ts +233 -0
- package/test/components/card.test.ts +560 -0
- package/test/components/carousel.test.ts +951 -0
- package/test/components/checkbox.test.ts +462 -0
- package/test/components/chip.test.ts +692 -0
- package/test/components/datepicker.test.ts +1124 -0
- package/test/components/dialog.test.ts +990 -0
- package/test/components/divider.test.ts +412 -0
- package/test/components/extended-fab.test.ts +672 -0
- package/test/components/fab.test.ts +561 -0
- package/test/components/list.test.ts +365 -0
- package/test/components/menu.test.ts +718 -0
- package/test/components/navigation.test.ts +186 -0
- package/test/components/progress.test.ts +567 -0
- package/test/components/radios.test.ts +699 -0
- package/test/components/search.test.ts +1135 -0
- package/test/components/segmented-button.test.ts +732 -0
- package/test/components/sheet.test.ts +641 -0
- package/test/components/slider.test.ts +1220 -0
- package/test/components/snackbar.test.ts +461 -0
- package/test/components/switch.test.ts +452 -0
- package/test/components/tabs.test.ts +1369 -0
- package/test/components/textfield.test.ts +400 -0
- package/test/components/timepicker.test.ts +592 -0
- package/test/components/tooltip.test.ts +630 -0
- package/test/components/top-app-bar.test.ts +566 -0
- package/test/core/dom.attributes.test.ts +148 -0
- package/test/core/dom.classes.test.ts +152 -0
- package/test/core/dom.events.test.ts +243 -0
- package/test/core/emitter.test.ts +141 -0
- package/test/core/ripple.test.ts +99 -0
- package/test/core/state.store.test.ts +189 -0
- package/test/core/utils.normalize.test.ts +61 -0
- package/test/core/utils.object.test.ts +120 -0
- package/test/setup.ts +451 -0
- package/tsconfig.json +2 -2
- package/src/components/snackbar/constants.ts +0 -26
- package/test/components/button.test.js +0 -170
- package/test/components/checkbox.test.js +0 -238
- package/test/components/list.test.js +0 -105
- package/test/components/menu.test.js +0 -385
- package/test/components/navigation.test.js +0 -227
- package/test/components/snackbar.test.js +0 -234
- package/test/components/switch.test.js +0 -186
- package/test/components/textfield.test.js +0 -314
- package/test/core/emitter.test.js +0 -141
- package/test/core/ripple.test.js +0 -66
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// test/components/bottom-app-bar.test.ts
|
|
2
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
|
|
5
|
+
// Set up JSDOM
|
|
6
|
+
const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`);
|
|
7
|
+
global.document = dom.window.document;
|
|
8
|
+
global.window = dom.window;
|
|
9
|
+
global.Element = dom.window.Element;
|
|
10
|
+
global.HTMLElement = dom.window.HTMLElement;
|
|
11
|
+
global.Event = dom.window.Event;
|
|
12
|
+
global.CustomEvent = dom.window.CustomEvent;
|
|
13
|
+
|
|
14
|
+
// Import types directly to avoid circular dependencies
|
|
15
|
+
import type {
|
|
16
|
+
BottomAppBar,
|
|
17
|
+
BottomAppBarConfig,
|
|
18
|
+
FabPosition
|
|
19
|
+
} from '../../src/components/bottom-app-bar/types';
|
|
20
|
+
|
|
21
|
+
// Define constants here to avoid circular dependencies
|
|
22
|
+
const FAB_POSITIONS = {
|
|
23
|
+
CENTER: 'center',
|
|
24
|
+
END: 'end'
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
// Create a mock bottom app bar implementation
|
|
28
|
+
const createMockBottomAppBar = (config: BottomAppBarConfig = {}): BottomAppBar => {
|
|
29
|
+
// Default configuration
|
|
30
|
+
const defaultConfig: BottomAppBarConfig = {
|
|
31
|
+
tag: 'div',
|
|
32
|
+
hasFab: false,
|
|
33
|
+
fabPosition: FAB_POSITIONS.END,
|
|
34
|
+
autoHide: false,
|
|
35
|
+
transitionDuration: 300,
|
|
36
|
+
prefix: 'mtrl',
|
|
37
|
+
componentName: 'bottom-app-bar'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Merge with user configuration
|
|
41
|
+
const mergedConfig = {
|
|
42
|
+
...defaultConfig,
|
|
43
|
+
...config
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Create main element
|
|
47
|
+
const element = document.createElement(mergedConfig.tag || 'div');
|
|
48
|
+
element.className = `${mergedConfig.prefix}-${mergedConfig.componentName}`;
|
|
49
|
+
|
|
50
|
+
// Add custom class if provided
|
|
51
|
+
if (mergedConfig.class) {
|
|
52
|
+
element.className += ` ${mergedConfig.class}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add FAB position class if applicable
|
|
56
|
+
if (mergedConfig.hasFab) {
|
|
57
|
+
element.className += ` ${mergedConfig.prefix}-${mergedConfig.componentName}--fab-${mergedConfig.fabPosition}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create actions container
|
|
61
|
+
const actionsContainer = document.createElement('div');
|
|
62
|
+
actionsContainer.className = `${mergedConfig.prefix}-${mergedConfig.componentName}__actions`;
|
|
63
|
+
element.appendChild(actionsContainer);
|
|
64
|
+
|
|
65
|
+
// Create FAB container if needed
|
|
66
|
+
let fabContainer: HTMLElement | null = null;
|
|
67
|
+
if (mergedConfig.hasFab) {
|
|
68
|
+
fabContainer = document.createElement('div');
|
|
69
|
+
fabContainer.className = `${mergedConfig.prefix}-${mergedConfig.componentName}__fab`;
|
|
70
|
+
element.appendChild(fabContainer);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Event handlers
|
|
74
|
+
const eventHandlers: Record<string, Function[]> = {};
|
|
75
|
+
|
|
76
|
+
// Set initial state
|
|
77
|
+
let isVisible = true;
|
|
78
|
+
|
|
79
|
+
// Create the component instance
|
|
80
|
+
const bottomAppBar: BottomAppBar = {
|
|
81
|
+
element,
|
|
82
|
+
config: mergedConfig,
|
|
83
|
+
|
|
84
|
+
addAction: (button: HTMLElement) => {
|
|
85
|
+
actionsContainer.appendChild(button);
|
|
86
|
+
return bottomAppBar;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
addFab: (fab: HTMLElement) => {
|
|
90
|
+
if (!mergedConfig.hasFab) {
|
|
91
|
+
mergedConfig.hasFab = true;
|
|
92
|
+
|
|
93
|
+
// Add class if not present
|
|
94
|
+
if (!element.classList.contains(`${mergedConfig.prefix}-${mergedConfig.componentName}--fab-${mergedConfig.fabPosition}`)) {
|
|
95
|
+
element.classList.add(`${mergedConfig.prefix}-${mergedConfig.componentName}--fab-${mergedConfig.fabPosition}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create FAB container if needed
|
|
99
|
+
if (!fabContainer) {
|
|
100
|
+
fabContainer = document.createElement('div');
|
|
101
|
+
fabContainer.className = `${mergedConfig.prefix}-${mergedConfig.componentName}__fab`;
|
|
102
|
+
element.appendChild(fabContainer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (fabContainer) {
|
|
107
|
+
// Clear existing fab if any
|
|
108
|
+
fabContainer.innerHTML = '';
|
|
109
|
+
fabContainer.appendChild(fab);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return bottomAppBar;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
show: () => {
|
|
116
|
+
element.style.transform = '';
|
|
117
|
+
element.style.opacity = '1';
|
|
118
|
+
isVisible = true;
|
|
119
|
+
|
|
120
|
+
// Call callback if provided
|
|
121
|
+
if (mergedConfig.onVisibilityChange) {
|
|
122
|
+
mergedConfig.onVisibilityChange(true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return bottomAppBar;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
hide: () => {
|
|
129
|
+
element.style.transform = 'translateY(100%)';
|
|
130
|
+
element.style.opacity = '0';
|
|
131
|
+
isVisible = false;
|
|
132
|
+
|
|
133
|
+
// Call callback if provided
|
|
134
|
+
if (mergedConfig.onVisibilityChange) {
|
|
135
|
+
mergedConfig.onVisibilityChange(false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return bottomAppBar;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
isVisible: () => isVisible,
|
|
142
|
+
|
|
143
|
+
getActionsContainer: () => actionsContainer,
|
|
144
|
+
|
|
145
|
+
// Add standard lifecycle methods
|
|
146
|
+
on: (event: string, handler: Function) => {
|
|
147
|
+
if (!eventHandlers[event]) {
|
|
148
|
+
eventHandlers[event] = [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
eventHandlers[event].push(handler);
|
|
152
|
+
return bottomAppBar;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
off: (event: string, handler: Function) => {
|
|
156
|
+
if (!eventHandlers[event]) return bottomAppBar;
|
|
157
|
+
|
|
158
|
+
eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
|
|
159
|
+
return bottomAppBar;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
emit: (event: string, data?: any) => {
|
|
163
|
+
if (!eventHandlers[event]) return;
|
|
164
|
+
|
|
165
|
+
eventHandlers[event].forEach(handler => handler(data));
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
destroy: () => {
|
|
169
|
+
// Remove element from DOM if attached
|
|
170
|
+
if (element.parentNode) {
|
|
171
|
+
element.parentNode.removeChild(element);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Clear event handlers
|
|
175
|
+
Object.keys(eventHandlers).forEach(key => {
|
|
176
|
+
eventHandlers[key] = [];
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return bottomAppBar;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
describe('Bottom App Bar Component', () => {
|
|
185
|
+
test('should create a bottom app bar element', () => {
|
|
186
|
+
const bar = createMockBottomAppBar();
|
|
187
|
+
expect(bar.element).toBeDefined();
|
|
188
|
+
expect(bar.element.tagName).toBe('DIV');
|
|
189
|
+
expect(bar.element.className).toContain('mtrl-bottom-app-bar');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('should apply custom class', () => {
|
|
193
|
+
const customClass = 'custom-bottom-bar';
|
|
194
|
+
const bar = createMockBottomAppBar({
|
|
195
|
+
class: customClass
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(bar.element.className).toContain(customClass);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('should support FAB in center position', () => {
|
|
202
|
+
const bar = createMockBottomAppBar({
|
|
203
|
+
hasFab: true,
|
|
204
|
+
fabPosition: FAB_POSITIONS.CENTER
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(bar.element.className).toContain('mtrl-bottom-app-bar--fab-center');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('should support FAB in end position', () => {
|
|
211
|
+
const bar = createMockBottomAppBar({
|
|
212
|
+
hasFab: true,
|
|
213
|
+
fabPosition: FAB_POSITIONS.END
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(bar.element.className).toContain('mtrl-bottom-app-bar--fab-end');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('should add action buttons', () => {
|
|
220
|
+
const bar = createMockBottomAppBar();
|
|
221
|
+
const button = document.createElement('button');
|
|
222
|
+
button.className = 'action-button';
|
|
223
|
+
|
|
224
|
+
bar.addAction(button);
|
|
225
|
+
|
|
226
|
+
const actionsContainer = bar.getActionsContainer();
|
|
227
|
+
expect(actionsContainer.children.length).toBe(1);
|
|
228
|
+
expect(actionsContainer.children[0]).toBe(button);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should add FAB', () => {
|
|
232
|
+
const bar = createMockBottomAppBar();
|
|
233
|
+
const fab = document.createElement('button');
|
|
234
|
+
fab.className = 'fab-button';
|
|
235
|
+
|
|
236
|
+
bar.addFab(fab);
|
|
237
|
+
|
|
238
|
+
// Adding a FAB should automatically set hasFab to true
|
|
239
|
+
expect(bar.element.className).toContain('mtrl-bottom-app-bar--fab-end');
|
|
240
|
+
|
|
241
|
+
// The FAB should be inside the bar
|
|
242
|
+
const fabElement = bar.element.querySelector('.mtrl-bottom-app-bar__fab button.fab-button');
|
|
243
|
+
expect(fabElement).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should support show and hide methods', () => {
|
|
247
|
+
const bar = createMockBottomAppBar();
|
|
248
|
+
|
|
249
|
+
// Initial state should be visible
|
|
250
|
+
expect(bar.isVisible()).toBe(true);
|
|
251
|
+
|
|
252
|
+
// Hide the bar
|
|
253
|
+
bar.hide();
|
|
254
|
+
expect(bar.isVisible()).toBe(false);
|
|
255
|
+
expect(bar.element.style.transform).toBe('translateY(100%)');
|
|
256
|
+
expect(bar.element.style.opacity).toBe('0');
|
|
257
|
+
|
|
258
|
+
// Show the bar
|
|
259
|
+
bar.show();
|
|
260
|
+
expect(bar.isVisible()).toBe(true);
|
|
261
|
+
expect(bar.element.style.transform).toBe('');
|
|
262
|
+
expect(bar.element.style.opacity).toBe('1');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('should call visibility change callback', () => {
|
|
266
|
+
const onVisibilityChange = mock((visible: boolean) => {});
|
|
267
|
+
const bar = createMockBottomAppBar({
|
|
268
|
+
onVisibilityChange
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
bar.hide();
|
|
272
|
+
expect(onVisibilityChange).toHaveBeenCalledWith(false);
|
|
273
|
+
|
|
274
|
+
bar.show();
|
|
275
|
+
expect(onVisibilityChange).toHaveBeenCalledWith(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('should have an actions container', () => {
|
|
279
|
+
const bar = createMockBottomAppBar();
|
|
280
|
+
const actionsContainer = bar.getActionsContainer();
|
|
281
|
+
|
|
282
|
+
expect(actionsContainer).toBeDefined();
|
|
283
|
+
expect(actionsContainer.className).toContain('mtrl-bottom-app-bar__actions');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('should support custom tag', () => {
|
|
287
|
+
const bar = createMockBottomAppBar({
|
|
288
|
+
tag: 'footer'
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(bar.element.tagName).toBe('FOOTER');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('should clean up resources on destroy', () => {
|
|
295
|
+
const bar = createMockBottomAppBar();
|
|
296
|
+
const parent = document.createElement('div');
|
|
297
|
+
parent.appendChild(bar.element);
|
|
298
|
+
|
|
299
|
+
bar.destroy();
|
|
300
|
+
|
|
301
|
+
expect(parent.children.length).toBe(0);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// test/components/button.test.ts
|
|
2
|
+
import { describe, test, expect, mock, beforeAll, afterAll } from 'bun:test';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
|
|
5
|
+
// IMPORTANT: Due to circular dependencies in the actual button component
|
|
6
|
+
// we are using a mock implementation for tests. For a full implementation
|
|
7
|
+
// with the actual component, see test/ts/components/button.test.ts
|
|
8
|
+
|
|
9
|
+
// Setup jsdom environment
|
|
10
|
+
let dom: JSDOM;
|
|
11
|
+
let window: Window;
|
|
12
|
+
let document: Document;
|
|
13
|
+
let originalGlobalDocument: any;
|
|
14
|
+
let originalGlobalWindow: any;
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
// Create a new JSDOM instance
|
|
18
|
+
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
19
|
+
url: 'http://localhost/',
|
|
20
|
+
pretendToBeVisual: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Get window and document from jsdom
|
|
24
|
+
window = dom.window;
|
|
25
|
+
document = window.document;
|
|
26
|
+
|
|
27
|
+
// Store original globals
|
|
28
|
+
originalGlobalDocument = global.document;
|
|
29
|
+
originalGlobalWindow = global.window;
|
|
30
|
+
|
|
31
|
+
// Set globals to use jsdom
|
|
32
|
+
global.document = document;
|
|
33
|
+
global.window = window;
|
|
34
|
+
global.Element = window.Element;
|
|
35
|
+
global.HTMLElement = window.HTMLElement;
|
|
36
|
+
global.HTMLButtonElement = window.HTMLButtonElement;
|
|
37
|
+
global.Event = window.Event;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
// Restore original globals
|
|
42
|
+
global.document = originalGlobalDocument;
|
|
43
|
+
global.window = originalGlobalWindow;
|
|
44
|
+
|
|
45
|
+
// Clean up jsdom
|
|
46
|
+
window.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Mock button component factory
|
|
50
|
+
const createButton = (config: any = {}) => {
|
|
51
|
+
const element = document.createElement('button');
|
|
52
|
+
element.className = `mtrl-button ${config.variant ? `mtrl-button--${config.variant}` : ''}`;
|
|
53
|
+
|
|
54
|
+
if (config.disabled) {
|
|
55
|
+
element.setAttribute('disabled', '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (config.text) {
|
|
59
|
+
const textElement = document.createElement('span');
|
|
60
|
+
textElement.className = 'mtrl-button-text';
|
|
61
|
+
textElement.textContent = config.text;
|
|
62
|
+
element.appendChild(textElement);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.icon) {
|
|
66
|
+
const iconElement = document.createElement('span');
|
|
67
|
+
iconElement.className = 'mtrl-button-icon';
|
|
68
|
+
iconElement.innerHTML = config.icon;
|
|
69
|
+
element.appendChild(iconElement);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const eventHandlers: Record<string, Function[]> = {};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
element,
|
|
76
|
+
setText(text: string) {
|
|
77
|
+
let textElement = element.querySelector('.mtrl-button-text');
|
|
78
|
+
if (!textElement) {
|
|
79
|
+
textElement = document.createElement('span');
|
|
80
|
+
textElement.className = 'mtrl-button-text';
|
|
81
|
+
element.appendChild(textElement);
|
|
82
|
+
}
|
|
83
|
+
textElement.textContent = text;
|
|
84
|
+
return this;
|
|
85
|
+
},
|
|
86
|
+
getText() {
|
|
87
|
+
const textElement = element.querySelector('.mtrl-button-text');
|
|
88
|
+
return textElement ? textElement.textContent || '' : '';
|
|
89
|
+
},
|
|
90
|
+
setIcon(html: string) {
|
|
91
|
+
let iconElement = element.querySelector('.mtrl-button-icon');
|
|
92
|
+
if (!iconElement) {
|
|
93
|
+
iconElement = document.createElement('span');
|
|
94
|
+
iconElement.className = 'mtrl-button-icon';
|
|
95
|
+
element.appendChild(iconElement);
|
|
96
|
+
}
|
|
97
|
+
iconElement.innerHTML = html;
|
|
98
|
+
return this;
|
|
99
|
+
},
|
|
100
|
+
getIcon() {
|
|
101
|
+
const iconElement = element.querySelector('.mtrl-button-icon');
|
|
102
|
+
return iconElement ? iconElement.innerHTML : '';
|
|
103
|
+
},
|
|
104
|
+
enable() {
|
|
105
|
+
element.removeAttribute('disabled');
|
|
106
|
+
return this;
|
|
107
|
+
},
|
|
108
|
+
disable() {
|
|
109
|
+
element.setAttribute('disabled', '');
|
|
110
|
+
return this;
|
|
111
|
+
},
|
|
112
|
+
on(event: string, handler: Function) {
|
|
113
|
+
if (!eventHandlers[event]) {
|
|
114
|
+
eventHandlers[event] = [];
|
|
115
|
+
}
|
|
116
|
+
eventHandlers[event].push(handler);
|
|
117
|
+
element.addEventListener(event, handler as EventListener);
|
|
118
|
+
return this;
|
|
119
|
+
},
|
|
120
|
+
off(event: string, handler: Function) {
|
|
121
|
+
if (eventHandlers[event]) {
|
|
122
|
+
eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
|
|
123
|
+
}
|
|
124
|
+
element.removeEventListener(event, handler as EventListener);
|
|
125
|
+
return this;
|
|
126
|
+
},
|
|
127
|
+
destroy() {
|
|
128
|
+
// Clean up event handlers
|
|
129
|
+
Object.entries(eventHandlers).forEach(([event, handlers]) => {
|
|
130
|
+
handlers.forEach(handler => {
|
|
131
|
+
element.removeEventListener(event, handler as EventListener);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Remove from DOM
|
|
136
|
+
if (element.parentNode) {
|
|
137
|
+
element.parentNode.removeChild(element);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
describe('Button Component', () => {
|
|
144
|
+
test('should create a button element', () => {
|
|
145
|
+
const button = createButton();
|
|
146
|
+
expect(button.element).toBeDefined();
|
|
147
|
+
expect(button.element.tagName).toBe('BUTTON');
|
|
148
|
+
expect(button.element.className).toContain('mtrl-button');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should add text content', () => {
|
|
152
|
+
const buttonText = 'Click Me';
|
|
153
|
+
const button = createButton({
|
|
154
|
+
text: buttonText
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const textElement = button.element.querySelector('.mtrl-button-text');
|
|
158
|
+
expect(textElement).toBeDefined();
|
|
159
|
+
expect(textElement?.textContent).toBe(buttonText);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('should apply variant class', () => {
|
|
163
|
+
const variant = 'filled';
|
|
164
|
+
const button = createButton({
|
|
165
|
+
variant
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(button.element.className).toContain(`mtrl-button--${variant}`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should handle click events', () => {
|
|
172
|
+
const button = createButton();
|
|
173
|
+
const handleClick = mock(() => {});
|
|
174
|
+
|
|
175
|
+
button.on('click', handleClick);
|
|
176
|
+
|
|
177
|
+
// Simulate click event
|
|
178
|
+
const event = new Event('click');
|
|
179
|
+
button.element.dispatchEvent(event);
|
|
180
|
+
|
|
181
|
+
expect(handleClick).toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('should support disabled state', () => {
|
|
185
|
+
const button = createButton();
|
|
186
|
+
|
|
187
|
+
// Initially not disabled
|
|
188
|
+
expect(button.element.hasAttribute('disabled')).toBe(false);
|
|
189
|
+
|
|
190
|
+
// Disable the button
|
|
191
|
+
button.disable();
|
|
192
|
+
expect(button.element.hasAttribute('disabled')).toBe(true);
|
|
193
|
+
|
|
194
|
+
// Enable the button
|
|
195
|
+
button.enable();
|
|
196
|
+
expect(button.element.hasAttribute('disabled')).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should allow updating text', () => {
|
|
200
|
+
const button = createButton({
|
|
201
|
+
text: 'Initial'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const newText = 'Updated Text';
|
|
205
|
+
button.setText(newText);
|
|
206
|
+
|
|
207
|
+
const textElement = button.element.querySelector('.mtrl-button-text');
|
|
208
|
+
expect(textElement).toBeDefined();
|
|
209
|
+
expect(textElement?.textContent).toBe(newText);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should allow updating icon', () => {
|
|
213
|
+
const button = createButton();
|
|
214
|
+
|
|
215
|
+
const iconSvg = '<svg><path d="M10 10"></path></svg>';
|
|
216
|
+
button.setIcon(iconSvg);
|
|
217
|
+
|
|
218
|
+
const iconElement = button.element.querySelector('.mtrl-button-icon');
|
|
219
|
+
expect(iconElement).toBeDefined();
|
|
220
|
+
expect(iconElement?.innerHTML).toBe(iconSvg);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should properly clean up resources', () => {
|
|
224
|
+
const button = createButton();
|
|
225
|
+
const parentElement = document.createElement('div');
|
|
226
|
+
parentElement.appendChild(button.element);
|
|
227
|
+
|
|
228
|
+
// Destroy should remove the element and clean up resources
|
|
229
|
+
button.destroy();
|
|
230
|
+
|
|
231
|
+
expect(parentElement.children.length).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
});
|