angular-matecu 4.0.7 → 4.1.0

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 (126) hide show
  1. package/README.md +347 -130
  2. package/ng-package.json +7 -0
  3. package/package.json +6 -21
  4. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.html +11 -0
  5. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.scss +39 -0
  6. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.spec.ts +25 -0
  7. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.ts +60 -0
  8. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.html +24 -0
  9. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.scss +5 -0
  10. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.spec.ts +25 -0
  11. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.ts +53 -0
  12. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.html +10 -0
  13. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.scss +34 -0
  14. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.spec.ts +25 -0
  15. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.ts +45 -0
  16. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.html +36 -0
  17. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.scss +56 -0
  18. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.spec.ts +23 -0
  19. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.ts +336 -0
  20. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.html +64 -0
  21. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.scss +23 -0
  22. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.spec.ts +23 -0
  23. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.ts +314 -0
  24. package/src/lib/components/matecu-file-input/matecu-file-input-customization.md +284 -0
  25. package/src/lib/components/matecu-file-input/matecu-file-input.example.md +228 -0
  26. package/src/lib/components/matecu-file-input/matecu-file-input.html +128 -0
  27. package/src/lib/components/matecu-file-input/matecu-file-input.scss +461 -0
  28. package/src/lib/components/matecu-file-input/matecu-file-input.spec.ts +340 -0
  29. package/src/lib/components/matecu-file-input/matecu-file-input.ts +438 -0
  30. package/src/lib/components/matecu-spinner/matecu-spinner.component.css +15 -0
  31. package/src/lib/components/matecu-spinner/matecu-spinner.component.html +44 -0
  32. package/src/lib/components/matecu-spinner/matecu-spinner.component.spec.ts +25 -0
  33. package/src/lib/components/matecu-spinner/matecu-spinner.component.ts +54 -0
  34. package/src/lib/components/matecu-spinner/spinner-loader.component.scss +13 -0
  35. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.html +1 -0
  36. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.scss +19 -0
  37. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.spec.ts +25 -0
  38. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.ts +14 -0
  39. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.html +1 -0
  40. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.scss +14 -0
  41. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.spec.ts +25 -0
  42. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.ts +11 -0
  43. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.html +3 -0
  44. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.scss +19 -0
  45. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.spec.ts +25 -0
  46. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.ts +31 -0
  47. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.html +1 -0
  48. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.scss +8 -0
  49. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.spec.ts +23 -0
  50. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.ts +11 -0
  51. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.html +9 -0
  52. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.scss +34 -0
  53. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.spec.ts +23 -0
  54. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.ts +18 -0
  55. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.html +7 -0
  56. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.scss +49 -0
  57. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.spec.ts +25 -0
  58. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.ts +112 -0
  59. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.html +20 -0
  60. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.scss +90 -0
  61. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.spec.ts +25 -0
  62. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.ts +92 -0
  63. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.html +1 -0
  64. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.scss +91 -0
  65. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.spec.ts +25 -0
  66. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.ts +14 -0
  67. package/src/lib/modules/matecu-alert-box/matecu-alert-box.module.ts +16 -0
  68. package/src/lib/modules/matecu-spinner/matecu-spinner.module.ts +14 -0
  69. package/src/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.ts +45 -0
  70. package/src/lib/services/matecu-snack-bar.service.spec.ts +16 -0
  71. package/src/lib/services/matecu-snack-bar.service.ts +66 -0
  72. package/src/lib/services/matecu-spinner.service.spec.ts +16 -0
  73. package/src/lib/services/matecu-spinner.service.ts +39 -0
  74. package/src/lib/types/matecu-alert-dialog.ts +10 -0
  75. package/{lib/types/matecu-alert-snackbar.d.ts → src/lib/types/matecu-alert-snackbar.ts} +5 -4
  76. package/src/lib/types/matecu-altert-box-type.ts +6 -0
  77. package/src/lib/types/matecu-autocomplete.ts +5 -0
  78. package/{public-api.d.ts → src/public-api.ts} +14 -0
  79. package/tsconfig.lib.json +17 -0
  80. package/tsconfig.lib.prod.json +11 -0
  81. package/tsconfig.spec.json +15 -0
  82. package/CHANGELOG.md +0 -22
  83. package/esm2022/angular-matecu.mjs +0 -5
  84. package/esm2022/lib/components/matecu-alert-box/matecu-alert-box.component.mjs +0 -67
  85. package/esm2022/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.mjs +0 -54
  86. package/esm2022/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.mjs +0 -43
  87. package/esm2022/lib/components/matecu-spinner/matecu-spinner.component.mjs +0 -58
  88. package/esm2022/lib/components/matecu-topbar-action/matecu-topbar-action.component.mjs +0 -18
  89. package/esm2022/lib/components/matecu-topbar-body/matecu-topbar-body.component.mjs +0 -17
  90. package/esm2022/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.mjs +0 -43
  91. package/esm2022/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.mjs +0 -12
  92. package/esm2022/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.mjs +0 -29
  93. package/esm2022/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.mjs +0 -112
  94. package/esm2022/lib/components/matecu-topbar-search/matecu-topbar-search.component.mjs +0 -93
  95. package/esm2022/lib/components/matecu-topbar-title/matecu-topbar-title.component.mjs +0 -18
  96. package/esm2022/lib/modules/matecu-alert-box/matecu-alert-box.module.mjs +0 -24
  97. package/esm2022/lib/modules/matecu-spinner/matecu-spinner.module.mjs +0 -22
  98. package/esm2022/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.mjs +0 -83
  99. package/esm2022/lib/services/matecu-snack-bar.service.mjs +0 -66
  100. package/esm2022/lib/services/matecu-spinner.service.mjs +0 -44
  101. package/esm2022/lib/types/matecu-alert-dialog.mjs +0 -2
  102. package/esm2022/lib/types/matecu-alert-snackbar.mjs +0 -2
  103. package/esm2022/lib/types/matecu-altert-box-type.mjs +0 -8
  104. package/esm2022/public-api.mjs +0 -32
  105. package/fesm2022/angular-matecu.mjs +0 -735
  106. package/fesm2022/angular-matecu.mjs.map +0 -1
  107. package/index.d.ts +0 -5
  108. package/lib/components/matecu-alert-box/matecu-alert-box.component.d.ts +0 -19
  109. package/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.d.ts +0 -22
  110. package/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.d.ts +0 -20
  111. package/lib/components/matecu-spinner/matecu-spinner.component.d.ts +0 -20
  112. package/lib/components/matecu-topbar-action/matecu-topbar-action.component.d.ts +0 -9
  113. package/lib/components/matecu-topbar-body/matecu-topbar-body.component.d.ts +0 -6
  114. package/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.d.ts +0 -12
  115. package/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.d.ts +0 -5
  116. package/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.d.ts +0 -8
  117. package/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.d.ts +0 -27
  118. package/lib/components/matecu-topbar-search/matecu-topbar-search.component.d.ts +0 -27
  119. package/lib/components/matecu-topbar-title/matecu-topbar-title.component.d.ts +0 -9
  120. package/lib/modules/matecu-alert-box/matecu-alert-box.module.d.ts +0 -14
  121. package/lib/modules/matecu-spinner/matecu-spinner.module.d.ts +0 -8
  122. package/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.d.ts +0 -19
  123. package/lib/services/matecu-snack-bar.service.d.ts +0 -17
  124. package/lib/services/matecu-spinner.service.d.ts +0 -15
  125. package/lib/types/matecu-alert-dialog.d.ts +0 -9
  126. package/lib/types/matecu-altert-box-type.d.ts +0 -6
@@ -0,0 +1,340 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { DebugElement } from '@angular/core';
3
+ import { By } from '@angular/platform-browser';
4
+
5
+ import { MatecuFileInput, FileInputState } from './matecu-file-input';
6
+
7
+ describe('MatecuFileInput', () => {
8
+ let component: MatecuFileInput;
9
+ let fixture: ComponentFixture<MatecuFileInput>;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ imports: [MatecuFileInput],
14
+ }).compileComponents();
15
+
16
+ fixture = TestBed.createComponent(MatecuFileInput);
17
+ component = fixture.componentInstance;
18
+ fixture.detectChanges();
19
+ });
20
+
21
+ it('should create', () => {
22
+ expect(component).toBeTruthy();
23
+ });
24
+
25
+ it('should initialize with default values', () => {
26
+ expect(component.multiple).toBeFalse();
27
+ expect(component.maxFiles).toBe(1);
28
+ expect(component.enableDragDrop).toBeTrue();
29
+ expect(component.showPreview).toBeFalse();
30
+ expect(component.currentState).toBe(FileInputState.IDLE);
31
+ expect(component.files).toEqual([]);
32
+ expect(component.validationErrors).toEqual([]);
33
+ });
34
+
35
+ describe('File Validation', () => {
36
+ it('should validate file size correctly', () => {
37
+ component.maxFileSize = 1024; // 1KB
38
+ const largeFile = new File(['x'.repeat(2048)], 'large.txt', { type: 'text/plain' });
39
+ const smallFile = new File(['small'], 'small.txt', { type: 'text/plain' });
40
+
41
+ let result = component['validateFile'](largeFile);
42
+ expect(result.isValid).toBeFalse();
43
+ expect(result.errors).toContain(component.errorMessages.invalidSize);
44
+
45
+ result = component['validateFile'](smallFile);
46
+ expect(result.isValid).toBeTrue();
47
+ expect(result.errors).toEqual([]);
48
+ });
49
+
50
+ it('should validate MIME types correctly', () => {
51
+ component.acceptedMimeTypes = ['image/jpeg', 'image/png'];
52
+ const validFile = new File(['image'], 'image.jpg', { type: 'image/jpeg' });
53
+ const invalidFile = new File(['text'], 'doc.txt', { type: 'text/plain' });
54
+
55
+ let result = component['validateFile'](validFile);
56
+ expect(result.isValid).toBeTrue();
57
+
58
+ result = component['validateFile'](invalidFile);
59
+ expect(result.isValid).toBeFalse();
60
+ expect(result.errors).toContain(component.errorMessages.invalidType);
61
+ });
62
+
63
+ it('should validate file extensions correctly', () => {
64
+ component.acceptedExtensions = ['.jpg', '.png'];
65
+ const validFile = new File(['image'], 'image.jpg', { type: 'image/jpeg' });
66
+ const invalidFile = new File(['text'], 'doc.txt', { type: 'text/plain' });
67
+
68
+ let result = component['validateFile'](validFile);
69
+ expect(result.isValid).toBeTrue();
70
+
71
+ result = component['validateFile'](invalidFile);
72
+ expect(result.isValid).toBeFalse();
73
+ });
74
+ });
75
+
76
+ describe('Single File Mode', () => {
77
+ beforeEach(() => {
78
+ component.multiple = false;
79
+ });
80
+
81
+ it('should handle single file selection', () => {
82
+ spyOn(component.fileSelected, 'emit');
83
+ const file = new File(['content'], 'test.txt', { type: 'text/plain' });
84
+
85
+ component['handleSingleFile'](file);
86
+
87
+ expect(component.file).toBe(file);
88
+ expect(component.fileName).toBe('test.txt');
89
+ expect(component.selectedFileSize).toBeGreaterThan(0);
90
+ });
91
+
92
+ it('should use display name when provided', () => {
93
+ component.displayName = 'Custom Name';
94
+ const file = new File(['content'], 'test.txt', { type: 'text/plain' });
95
+
96
+ component['handleSingleFile'](file);
97
+
98
+ expect(component.fileName).toBe('Custom Name');
99
+ });
100
+
101
+ it('should remove file correctly', () => {
102
+ const file = new File(['content'], 'test.txt', { type: 'text/plain' });
103
+ component.file = file;
104
+ spyOn(component.fileRemoved, 'emit');
105
+ spyOn(component, 'resetComponent').and.callThrough();
106
+ spyOn(component, 'notifyChange').and.callThrough();
107
+
108
+ component.removeFile(file);
109
+
110
+ expect(component.fileRemoved.emit).toHaveBeenCalledWith(file);
111
+ expect(component.resetComponent).toHaveBeenCalled();
112
+ expect(component.notifyChange).toHaveBeenCalledWith(null);
113
+ });
114
+ });
115
+
116
+ describe('Multiple Files Mode', () => {
117
+ beforeEach(() => {
118
+ component.multiple = true;
119
+ component.maxFiles = 3;
120
+ });
121
+
122
+ it('should handle multiple file selection', () => {
123
+ const files = [
124
+ new File(['content1'], 'test1.txt', { type: 'text/plain' }),
125
+ new File(['content2'], 'test2.txt', { type: 'text/plain' }),
126
+ ];
127
+
128
+ component['handleMultipleFiles'](files);
129
+
130
+ expect(component.files).toEqual(files);
131
+ expect(component.file).toBe(files[0]);
132
+ expect(component.fileName).toContain('2 archivo(s)');
133
+ });
134
+
135
+ it('should validate max files limit', async () => {
136
+ const files = [
137
+ new File(['1'], 'test1.txt', { type: 'text/plain' }),
138
+ new File(['2'], 'test2.txt', { type: 'text/plain' }),
139
+ new File(['3'], 'test3.txt', { type: 'text/plain' }),
140
+ new File(['4'], 'test4.txt', { type: 'text/plain' }), // Exceeds limit
141
+ ];
142
+
143
+ await component['handleFileSelection'](files);
144
+
145
+ expect(component.validationErrors).toContain(component.errorMessages.tooManyFiles);
146
+ expect(component.currentState).toBe(FileInputState.ERROR);
147
+ });
148
+
149
+ it('should remove specific file from multiple selection', () => {
150
+ const file1 = new File(['1'], 'test1.txt', { type: 'text/plain' });
151
+ const file2 = new File(['2'], 'test2.txt', { type: 'text/plain' });
152
+ component.files = [file1, file2];
153
+
154
+ spyOn(component.filesSelected, 'emit');
155
+ spyOn(component.fileRemoved, 'emit');
156
+
157
+ component.removeFile(file1);
158
+
159
+ expect(component.files).toEqual([file2]);
160
+ expect(component.filesSelected.emit).toHaveBeenCalledWith([file2]);
161
+ expect(component.fileRemoved.emit).toHaveBeenCalledWith(file1);
162
+ });
163
+ });
164
+
165
+ describe('Drag and Drop', () => {
166
+ let dragEvent: DragEvent;
167
+
168
+ beforeEach(() => {
169
+ dragEvent = new DragEvent('dragover');
170
+ Object.defineProperty(dragEvent, 'dataTransfer', {
171
+ value: {
172
+ files: [new File(['test'], 'test.txt', { type: 'text/plain' })],
173
+ },
174
+ });
175
+ component.enableDragDrop = true;
176
+ });
177
+
178
+ it('should handle drag over event', () => {
179
+ spyOn(dragEvent, 'preventDefault');
180
+ spyOn(component.dragEnter, 'emit');
181
+
182
+ component.onDragOver(dragEvent);
183
+
184
+ expect(dragEvent.preventDefault).toHaveBeenCalled();
185
+ expect(component.isDragOver).toBeTrue();
186
+ expect(component.dragEnter.emit).toHaveBeenCalledWith(dragEvent);
187
+ });
188
+
189
+ it('should handle drag leave event', () => {
190
+ component.isDragOver = true;
191
+ spyOn(component.dragLeave, 'emit');
192
+
193
+ component.onDragLeave(dragEvent);
194
+
195
+ expect(component.isDragOver).toBeFalse();
196
+ expect(component.dragLeave.emit).toHaveBeenCalledWith(dragEvent);
197
+ });
198
+
199
+ it('should handle drop event', () => {
200
+ spyOn(dragEvent, 'preventDefault');
201
+ spyOn(component, 'handleFileSelection').and.returnValue(Promise.resolve());
202
+
203
+ component.onDrop(dragEvent);
204
+
205
+ expect(dragEvent.preventDefault).toHaveBeenCalled();
206
+ expect(component.isDragOver).toBeFalse();
207
+ expect(component.handleFileSelection).toHaveBeenCalled();
208
+ });
209
+
210
+ it('should ignore drag events when disabled', () => {
211
+ component.enableDragDrop = false;
212
+ spyOn(dragEvent, 'preventDefault');
213
+
214
+ component.onDragOver(dragEvent);
215
+
216
+ expect(dragEvent.preventDefault).not.toHaveBeenCalled();
217
+ expect(component.isDragOver).toBeFalse();
218
+ });
219
+ });
220
+
221
+ describe('File Icon and Preview', () => {
222
+ it('should return correct icon for different file types', () => {
223
+ const imageFile = new File([''], 'image.jpg', { type: 'image/jpeg' });
224
+ const pdfFile = new File([''], 'document.pdf', { type: 'application/pdf' });
225
+ const unknownFile = new File([''], 'file.xyz', { type: 'unknown/type' });
226
+
227
+ expect(component.getFileIcon(imageFile)).toBe('🖼️');
228
+ expect(component.getFileIcon(pdfFile)).toBe('📄');
229
+ expect(component.getFileIcon(unknownFile)).toBe('📄');
230
+ });
231
+
232
+ it('should generate preview URL for images', () => {
233
+ spyOn(URL, 'createObjectURL').and.returnValue('blob:test-url');
234
+ const imageFile = new File([''], 'image.jpg', { type: 'image/jpeg' });
235
+
236
+ const previewUrl = component.getPreviewUrl(imageFile);
237
+
238
+ expect(URL.createObjectURL).toHaveBeenCalledWith(imageFile);
239
+ expect(previewUrl).toBe('blob:test-url');
240
+ });
241
+
242
+ it('should cache preview URLs', () => {
243
+ spyOn(URL, 'createObjectURL').and.returnValue('blob:test-url');
244
+ const imageFile = new File([''], 'image.jpg', { type: 'image/jpeg' });
245
+
246
+ const url1 = component.getPreviewUrl(imageFile);
247
+ const url2 = component.getPreviewUrl(imageFile);
248
+
249
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
250
+ expect(url1).toBe(url2);
251
+ });
252
+ });
253
+
254
+ describe('State Management', () => {
255
+ it('should emit state changes', () => {
256
+ spyOn(component.stateChange, 'emit');
257
+
258
+ component['changeState'](FileInputState.LOADING);
259
+
260
+ expect(component.currentState).toBe(FileInputState.LOADING);
261
+ expect(component.stateChange.emit).toHaveBeenCalledWith(FileInputState.LOADING);
262
+ });
263
+
264
+ it('should not emit same state twice', () => {
265
+ component.currentState = FileInputState.LOADING;
266
+ spyOn(component.stateChange, 'emit');
267
+
268
+ component['changeState'](FileInputState.LOADING);
269
+
270
+ expect(component.stateChange.emit).not.toHaveBeenCalled();
271
+ });
272
+
273
+ it('should return correct computed properties', () => {
274
+ component.validationErrors = ['error'];
275
+ component.currentState = FileInputState.ERROR;
276
+ component.files = [new File([''], 'test.txt')];
277
+
278
+ expect(component.hasErrors).toBeTrue();
279
+ expect(component.isLoading).toBeFalse();
280
+ expect(component.hasFiles).toBeTrue();
281
+ });
282
+ });
283
+
284
+ describe('ControlValueAccessor', () => {
285
+ it('should write single file value', () => {
286
+ const file = new File(['content'], 'test.txt', { type: 'text/plain' });
287
+ spyOn(component, 'handleSingleFile').and.callThrough();
288
+
289
+ component.writeValue(file);
290
+
291
+ expect(component.handleSingleFile).toHaveBeenCalledWith(file);
292
+ });
293
+
294
+ it('should write multiple files value', () => {
295
+ const files = [new File(['1'], 'test1.txt'), new File(['2'], 'test2.txt')];
296
+ spyOn(component, 'handleMultipleFiles').and.callThrough();
297
+
298
+ component.writeValue(files);
299
+
300
+ expect(component.handleMultipleFiles).toHaveBeenCalledWith(files);
301
+ });
302
+
303
+ it('should register change and touched callbacks', () => {
304
+ const changeFn = jasmine.createSpy();
305
+ const touchedFn = jasmine.createSpy();
306
+
307
+ component.registerOnChange(changeFn);
308
+ component.registerOnTouched(touchedFn);
309
+
310
+ expect(component['onChange']).toBe(changeFn);
311
+ expect(component['onTouched']).toBe(touchedFn);
312
+ });
313
+ });
314
+
315
+ describe('Memory Management', () => {
316
+ it('should cleanup preview URLs on destroy', () => {
317
+ spyOn(URL, 'revokeObjectURL');
318
+ component.previewUrl = 'blob:test-url';
319
+ component.previewUrls.set('key1', 'blob:url1');
320
+ component.previewUrls.set('key2', 'blob:url2');
321
+
322
+ component.ngOnDestroy();
323
+
324
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
325
+ expect(component.previewUrls.size).toBe(0);
326
+ });
327
+
328
+ it('should cleanup preview URL when removing file', () => {
329
+ spyOn(URL, 'revokeObjectURL');
330
+ const file = new File([''], 'test.jpg', { type: 'image/jpeg' });
331
+ const fileKey = `${file.name}-${file.size}-${file.lastModified}`;
332
+ component.previewUrls.set(fileKey, 'blob:test-url');
333
+
334
+ component.removeFile(file);
335
+
336
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:test-url');
337
+ expect(component.previewUrls.has(fileKey)).toBeFalse();
338
+ });
339
+ });
340
+ });