@osimatic/helpers-js 1.5.3 → 1.5.4

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.
@@ -9,7 +9,7 @@ class MockFileReader {
9
9
  constructor() {
10
10
  this.readAsDataURL = jest.fn(function(file) {
11
11
  setTimeout(() => {
12
- if (file.type.startsWith('image/')) {
12
+ if (file.type && file.type.startsWith('image/')) {
13
13
  this.result = 'data:image/png;base64,iVBORw0KGgoAAAANS';
14
14
  }
15
15
  if (this.onload) {
@@ -22,366 +22,220 @@ class MockFileReader {
22
22
 
23
23
  global.FileReader = MockFileReader;
24
24
 
25
- // Mock Math.random for consistent IDs
26
25
  let mockRandomCounter = 0;
27
26
  const originalMathRandom = Math.random;
27
+ const MB = 1024 * 1024;
28
28
 
29
29
  describe('MultiFilesInput', () => {
30
- let mockFileInput;
31
- let mockFormGroup;
32
- let mockDropzone;
33
- let mockFilesPreview;
34
- let mockParent;
35
30
  let setFilesListSpy;
36
- let eventHandlers;
37
- let mockPreviewThumb;
38
- let mockBtnClose;
39
- let mockWrap;
31
+
32
+ function setupDOM(nbMaxFiles = 5, maxFileSize = MB) {
33
+ document.body.innerHTML = `
34
+ <div class="form-group">
35
+ <input type="file" id="fileInput" />
36
+ </div>
37
+ `;
38
+ const fileInput = document.getElementById('fileInput');
39
+ const formGroup = document.querySelector('.form-group');
40
+ setFilesListSpy = jest.fn();
41
+ MultiFilesInput.init(fileInput, setFilesListSpy, nbMaxFiles, maxFileSize);
42
+ return {
43
+ fileInput,
44
+ formGroup,
45
+ get dropzone() { return formGroup.querySelector('.multi_files_input_dropzone'); },
46
+ get filesPreview() { return formGroup.querySelector('.multi_files_input_files_preview'); },
47
+ get activeInput() { return formGroup.querySelector('input[type="file"]'); },
48
+ };
49
+ }
50
+
51
+ function triggerChange(activeInput, files) {
52
+ Object.defineProperty(activeInput, 'files', { value: files, configurable: true });
53
+ activeInput.dispatchEvent(new Event('change'));
54
+ }
55
+
56
+ function makeMockFile(name, size, type = 'text/plain') {
57
+ const f = new File(['x'], name, { type });
58
+ Object.defineProperty(f, 'size', { value: size });
59
+ Object.defineProperty(f, 'name', { value: name });
60
+ Object.defineProperty(f, 'type', { value: type });
61
+ return f;
62
+ }
40
63
 
41
64
  beforeEach(() => {
42
65
  jest.spyOn(FlashMessage, 'displayError');
43
-
44
- // Reset counter for consistent IDs
45
66
  mockRandomCounter = 0;
46
67
  Math.random = jest.fn(() => {
47
68
  mockRandomCounter++;
48
69
  return 0.123456789 + mockRandomCounter * 0.001;
49
70
  });
50
-
51
- // Setup event handlers storage
52
- eventHandlers = {
53
- click: null,
54
- dragover: null,
55
- dragleave: null,
56
- drop: null,
57
- change: null
58
- };
59
-
60
- // Setup preview mocks that will be reused
61
- mockPreviewThumb = {
62
- html: jest.fn().mockReturnThis()
63
- };
64
-
65
- mockBtnClose = {
66
- off: jest.fn().mockReturnThis(),
67
- on: jest.fn().mockReturnThis(),
68
- closest: jest.fn(() => ({
69
- index: jest.fn(() => 0),
70
- remove: jest.fn()
71
- }))
72
- };
73
-
74
- mockWrap = {
75
- find: jest.fn((selector) => {
76
- if (selector === '.preview-thumb') {
77
- return mockPreviewThumb;
78
- }
79
- if (selector === '.btn-close') {
80
- return mockBtnClose;
81
- }
82
- return {};
83
- }),
84
- closest: jest.fn(() => ({
85
- index: jest.fn(() => 0),
86
- remove: jest.fn()
87
- }))
88
- };
89
-
90
- // Mock jQuery elements
91
- mockFilesPreview = {
92
- append: jest.fn().mockReturnThis(),
93
- empty: jest.fn().mockReturnThis(),
94
- addClass: jest.fn().mockReturnThis(),
95
- removeClass: jest.fn().mockReturnThis(),
96
- length: 1
97
- };
98
-
99
- mockDropzone = {
100
- off: jest.fn().mockReturnThis(),
101
- on: jest.fn(function(event, handler) {
102
- eventHandlers[event] = handler;
103
- return this;
104
- }),
105
- addClass: jest.fn().mockReturnThis(),
106
- removeClass: jest.fn().mockReturnThis(),
107
- length: 0
108
- };
109
-
110
- mockParent = {
111
- find: jest.fn((selector) => {
112
- if (selector === '.multi_files_input_dropzone') {
113
- return mockDropzone;
114
- }
115
- if (selector === '.multi_files_input_files_preview') {
116
- return mockFilesPreview;
117
- }
118
- return { length: 0 };
119
- })
120
- };
121
-
122
- mockFormGroup = {
123
- find: jest.fn((selector) => {
124
- if (selector === '.multi_files_input_dropzone') {
125
- return { length: 0 };
126
- }
127
- if (selector === '.multi_files_input_files_preview') {
128
- return { length: 0 };
129
- }
130
- return { length: 0 };
131
- }),
132
- append: jest.fn()
133
- };
134
-
135
- mockFileInput = {
136
- closest: jest.fn(() => mockFormGroup),
137
- after: jest.fn(),
138
- parent: jest.fn(() => mockParent),
139
- addClass: jest.fn().mockReturnThis(),
140
- off: jest.fn().mockReturnThis(),
141
- on: jest.fn(function(event, handler) {
142
- eventHandlers[event] = handler;
143
- return this;
144
- }),
145
- trigger: jest.fn(),
146
- val: jest.fn().mockReturnThis()
147
- };
148
-
149
- // Mock jQuery $ function globally for all tests
150
- global.$ = jest.fn((selector) => {
151
- // Handle string selectors (element creation)
152
- if (typeof selector === 'string') {
153
- if (selector.includes('data-file-id')) {
154
- return mockWrap;
155
- }
156
- // Return a generic jQuery-like object for other selectors
157
- return {
158
- addClass: jest.fn().mockReturnThis(),
159
- removeClass: jest.fn().mockReturnThis()
160
- };
161
- }
162
- // Handle object selectors (wrapping existing elements like $(this))
163
- if (selector && typeof selector === 'object') {
164
- // If it's an element being wrapped, return jQuery-like object
165
- return {
166
- addClass: jest.fn().mockReturnThis(),
167
- removeClass: jest.fn().mockReturnThis(),
168
- closest: jest.fn(() => ({
169
- index: jest.fn(() => 0),
170
- remove: jest.fn()
171
- }))
172
- };
173
- }
174
- return {};
175
- });
176
-
177
- setFilesListSpy = jest.fn();
178
71
  });
179
72
 
180
73
  afterEach(() => {
181
74
  jest.restoreAllMocks();
182
75
  Math.random = originalMathRandom;
183
76
  jest.clearAllTimers();
77
+ document.body.innerHTML = '';
184
78
  });
185
79
 
186
80
  describe('init', () => {
187
81
  test('should create dropzone when it does not exist', () => {
188
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
189
-
190
- expect(mockFileInput.after).toHaveBeenCalledWith(expect.stringContaining('multi_files_input_dropzone'));
191
- expect(mockFileInput.after).toHaveBeenCalledWith(expect.stringContaining('Glissez-déposez vos fichiers ici'));
82
+ const { formGroup } = setupDOM();
83
+ const dropzone = formGroup.querySelector('.multi_files_input_dropzone');
84
+ expect(dropzone).not.toBeNull();
85
+ expect(dropzone.textContent).toContain('Glissez-déposez vos fichiers ici');
192
86
  });
193
87
 
194
88
  test('should not create dropzone when it already exists', () => {
195
- mockFormGroup.find = jest.fn((selector) => {
196
- if (selector === '.multi_files_input_dropzone') {
197
- return { length: 1 };
198
- }
199
- return { length: 0 };
200
- });
201
-
202
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
203
-
204
- expect(mockFileInput.after).not.toHaveBeenCalled();
89
+ document.body.innerHTML = `
90
+ <div class="form-group">
91
+ <input type="file" id="fileInput" />
92
+ <div class="multi_files_input_dropzone"></div>
93
+ </div>
94
+ `;
95
+ const fileInput = document.getElementById('fileInput');
96
+ const formGroup = document.querySelector('.form-group');
97
+ MultiFilesInput.init(fileInput, jest.fn(), 5, MB);
98
+ expect(formGroup.querySelectorAll('.multi_files_input_dropzone')).toHaveLength(1);
205
99
  });
206
100
 
207
101
  test('should create files preview container when it does not exist', () => {
208
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
209
-
210
- expect(mockFormGroup.append).toHaveBeenCalledWith(expect.stringContaining('multi_files_input_files_preview'));
102
+ const { formGroup } = setupDOM();
103
+ expect(formGroup.querySelector('.multi_files_input_files_preview')).not.toBeNull();
211
104
  });
212
105
 
213
106
  test('should not create preview container when it already exists', () => {
214
- mockFormGroup.find = jest.fn((selector) => {
215
- if (selector === '.multi_files_input_files_preview') {
216
- return { length: 1 };
217
- }
218
- return { length: 0 };
219
- });
220
-
221
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
222
-
223
- // Should only be called once for checking dropzone
224
- expect(mockFormGroup.find).toHaveBeenCalledTimes(2);
107
+ document.body.innerHTML = `
108
+ <div class="form-group">
109
+ <input type="file" id="fileInput" />
110
+ <div class="multi_files_input_files_preview"></div>
111
+ </div>
112
+ `;
113
+ const fileInput = document.getElementById('fileInput');
114
+ const formGroup = document.querySelector('.form-group');
115
+ MultiFilesInput.init(fileInput, jest.fn(), 5, MB);
116
+ expect(formGroup.querySelectorAll('.multi_files_input_files_preview')).toHaveLength(1);
225
117
  });
226
118
 
227
119
  test('should hide file input element', () => {
228
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
229
-
230
- expect(mockFileInput.addClass).toHaveBeenCalledWith('hide');
120
+ const { activeInput } = setupDOM();
121
+ expect(activeInput.classList.contains('hide')).toBe(true);
231
122
  });
232
123
 
233
124
  test('should empty files preview container on init', () => {
234
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
125
+ const { formGroup } = setupDOM();
126
+ const filesPreview = formGroup.querySelector('.multi_files_input_files_preview');
127
+ filesPreview.innerHTML = '<div>old content</div>';
128
+
129
+ // Re-init
130
+ const activeInput = formGroup.querySelector('input[type="file"]');
131
+ MultiFilesInput.init(activeInput, jest.fn(), 5, MB);
235
132
 
236
- expect(mockFilesPreview.empty).toHaveBeenCalled();
133
+ expect(formGroup.querySelector('.multi_files_input_files_preview').innerHTML).toBe('');
237
134
  });
238
135
 
239
136
  test('should setup click handler on dropzone', () => {
240
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
137
+ document.body.innerHTML = `
138
+ <div class="form-group">
139
+ <input type="file" id="fileInput" />
140
+ </div>
141
+ `;
142
+ const fileInput = document.getElementById('fileInput');
143
+ const formGroup = document.querySelector('.form-group');
144
+ const clickSpy = jest.spyOn(fileInput, 'click');
145
+
146
+ MultiFilesInput.init(fileInput, jest.fn(), 5, MB);
241
147
 
242
- expect(mockDropzone.off).toHaveBeenCalledWith('click');
243
- expect(mockDropzone.on).toHaveBeenCalledWith('click', expect.any(Function));
148
+ const dropzone = formGroup.querySelector('.multi_files_input_dropzone');
149
+ dropzone.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
150
+
151
+ expect(clickSpy).toHaveBeenCalled();
244
152
  });
245
153
 
246
154
  test('should setup dragover handler on dropzone', () => {
247
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
248
-
249
- expect(mockDropzone.off).toHaveBeenCalledWith('dragover');
250
- expect(mockDropzone.on).toHaveBeenCalledWith('dragover', expect.any(Function));
155
+ const { dropzone } = setupDOM();
156
+ dropzone.dispatchEvent(new Event('dragover', { cancelable: true }));
157
+ expect(dropzone.classList.contains('border-primary')).toBe(true);
251
158
  });
252
159
 
253
160
  test('should setup dragleave handler on dropzone', () => {
254
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
255
-
256
- expect(mockDropzone.off).toHaveBeenCalledWith('dragleave');
257
- expect(mockDropzone.on).toHaveBeenCalledWith('dragleave', expect.any(Function));
161
+ const { dropzone } = setupDOM();
162
+ dropzone.classList.add('border-primary');
163
+ dropzone.dispatchEvent(new Event('dragleave', { cancelable: true }));
164
+ expect(dropzone.classList.contains('border-primary')).toBe(false);
258
165
  });
259
166
 
260
167
  test('should setup drop handler on dropzone', () => {
261
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
168
+ const { dropzone } = setupDOM();
169
+ const mockFile = makeMockFile('test.txt', 500);
170
+
171
+ const dropEvent = new Event('drop', { cancelable: true });
172
+ Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [mockFile] } });
173
+ dropzone.dispatchEvent(dropEvent);
262
174
 
263
- expect(mockDropzone.off).toHaveBeenCalledWith('drop');
264
- expect(mockDropzone.on).toHaveBeenCalledWith('drop', expect.any(Function));
175
+ expect(setFilesListSpy).toHaveBeenCalled();
265
176
  });
266
177
 
267
178
  test('should setup change handler on file input', () => {
268
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
179
+ const { activeInput } = setupDOM();
180
+ const mockFile = makeMockFile('test.txt', 500);
269
181
 
270
- expect(mockFileInput.off).toHaveBeenCalledWith('change');
271
- expect(mockFileInput.on).toHaveBeenCalledWith('change', expect.any(Function));
182
+ triggerChange(activeInput, [mockFile]);
183
+
184
+ expect(setFilesListSpy).toHaveBeenCalled();
272
185
  });
273
186
  });
274
187
 
275
188
  describe('dropzone interactions', () => {
276
- beforeEach(() => {
277
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 5, 1024 * 1024);
278
- });
279
-
280
189
  test('should trigger file input click when dropzone is clicked', () => {
281
- const mockEvent = {
282
- preventDefault: jest.fn(),
283
- stopPropagation: jest.fn()
284
- };
190
+ document.body.innerHTML = `
191
+ <div class="form-group">
192
+ <input type="file" id="fileInput" />
193
+ </div>
194
+ `;
195
+ const fileInput = document.getElementById('fileInput');
196
+ const formGroup = document.querySelector('.form-group');
197
+ const clickSpy = jest.spyOn(fileInput, 'click');
198
+
199
+ MultiFilesInput.init(fileInput, jest.fn(), 5, MB);
285
200
 
286
- eventHandlers.click.call(mockDropzone, mockEvent);
201
+ const dropzone = formGroup.querySelector('.multi_files_input_dropzone');
202
+ dropzone.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
287
203
 
288
- expect(mockEvent.preventDefault).toHaveBeenCalled();
289
- expect(mockEvent.stopPropagation).toHaveBeenCalled();
290
- expect(mockFileInput.trigger).toHaveBeenCalledWith('click');
204
+ expect(clickSpy).toHaveBeenCalled();
291
205
  });
292
206
 
293
207
  test('should add border-primary class on dragover', () => {
294
- const mockEvent = {
295
- preventDefault: jest.fn(),
296
- stopPropagation: jest.fn()
297
- };
298
-
299
- // Use a proper jQuery-like context
300
- const contextWithAddClass = {
301
- addClass: jest.fn().mockReturnThis()
302
- };
303
-
304
- // Create a bound function that uses $ to return contextWithAddClass
305
- const handler = eventHandlers.dragover;
306
- const mockJQuery = jest.fn(() => contextWithAddClass);
307
- global.$ = mockJQuery;
308
-
309
- handler.call(contextWithAddClass, mockEvent);
310
-
311
- expect(mockEvent.preventDefault).toHaveBeenCalled();
312
- expect(mockEvent.stopPropagation).toHaveBeenCalled();
313
- expect(contextWithAddClass.addClass).toHaveBeenCalledWith('border-primary');
208
+ const { dropzone } = setupDOM();
209
+ dropzone.dispatchEvent(new Event('dragover', { cancelable: true }));
210
+ expect(dropzone.classList.contains('border-primary')).toBe(true);
314
211
  });
315
212
 
316
213
  test('should remove border-primary class on dragleave', () => {
317
- const mockEvent = {
318
- preventDefault: jest.fn(),
319
- stopPropagation: jest.fn()
320
- };
321
-
322
- const contextWithRemoveClass = {
323
- removeClass: jest.fn().mockReturnThis()
324
- };
325
-
326
- const handler = eventHandlers.dragleave;
327
- const mockJQuery = jest.fn(() => contextWithRemoveClass);
328
- global.$ = mockJQuery;
329
-
330
- handler.call(contextWithRemoveClass, mockEvent);
331
-
332
- expect(mockEvent.preventDefault).toHaveBeenCalled();
333
- expect(mockEvent.stopPropagation).toHaveBeenCalled();
334
- expect(contextWithRemoveClass.removeClass).toHaveBeenCalledWith('border-primary');
214
+ const { dropzone } = setupDOM();
215
+ dropzone.classList.add('border-primary');
216
+ dropzone.dispatchEvent(new Event('dragleave', { cancelable: true }));
217
+ expect(dropzone.classList.contains('border-primary')).toBe(false);
335
218
  });
336
219
 
337
220
  test('should handle file drop', () => {
338
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
339
- Object.defineProperty(mockFile, 'size', { value: 500 });
340
-
341
- const mockEvent = {
342
- preventDefault: jest.fn(),
343
- stopPropagation: jest.fn(),
344
- originalEvent: {
345
- dataTransfer: {
346
- files: [mockFile]
347
- }
348
- }
349
- };
350
-
351
- const contextElement = { id: 'dropzone-element' };
352
- const handler = eventHandlers.drop;
221
+ const { dropzone } = setupDOM();
222
+ const mockFile = makeMockFile('test.txt', 500);
353
223
 
354
- // Réinitialiser le spy $
355
- global.$.mockClear();
224
+ const dropEvent = new Event('drop', { cancelable: true });
225
+ Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [mockFile] } });
226
+ dropzone.dispatchEvent(dropEvent);
356
227
 
357
- handler.call(contextElement, mockEvent);
358
-
359
- expect(mockEvent.preventDefault).toHaveBeenCalled();
360
- expect(mockEvent.stopPropagation).toHaveBeenCalled();
361
- // Vérifie que $(this) a été appelé pour envelopper l'élément
362
- expect(global.$).toHaveBeenCalledWith(contextElement);
363
- // Le fichier devrait être ajouté à la liste
364
228
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
365
229
  });
366
230
 
367
231
  test('should handle drop with no files', () => {
368
- const mockEvent = {
369
- preventDefault: jest.fn(),
370
- stopPropagation: jest.fn(),
371
- originalEvent: {}
372
- };
373
-
374
- const contextWithRemoveClass = {
375
- removeClass: jest.fn().mockReturnThis()
376
- };
232
+ const { dropzone } = setupDOM();
377
233
 
378
- const handler = eventHandlers.drop;
379
- const mockJQuery = jest.fn(() => contextWithRemoveClass);
380
- global.$ = mockJQuery;
234
+ const dropEvent = new Event('drop', { cancelable: true });
235
+ Object.defineProperty(dropEvent, 'dataTransfer', { value: {} });
381
236
 
382
- // Should not throw error
383
237
  expect(() => {
384
- handler.call(contextWithRemoveClass, mockEvent);
238
+ dropzone.dispatchEvent(dropEvent);
385
239
  }).not.toThrow();
386
240
 
387
241
  expect(setFilesListSpy).not.toHaveBeenCalled();
@@ -389,625 +243,341 @@ describe('MultiFilesInput', () => {
389
243
  });
390
244
 
391
245
  describe('file handling', () => {
392
- beforeEach(() => {
393
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
394
- });
395
-
396
246
  test('should add valid file to files list', () => {
397
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
398
- Object.defineProperty(mockFile, 'size', { value: 500 });
399
-
400
- const mockEvent = {
401
- target: {
402
- files: [mockFile]
403
- }
404
- };
247
+ const { activeInput } = setupDOM(3, MB);
248
+ const mockFile = makeMockFile('test.txt', 500);
405
249
 
406
- eventHandlers.change(mockEvent);
250
+ triggerChange(activeInput, [mockFile]);
407
251
 
408
252
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
409
- expect(mockFileInput.val).toHaveBeenCalledWith('');
410
253
  });
411
254
 
412
255
  test('should not exceed maximum number of files', () => {
413
- const mockFile1 = new File(['content1'], 'test1.txt', { type: 'text/plain' });
414
- const mockFile2 = new File(['content2'], 'test2.txt', { type: 'text/plain' });
415
- const mockFile3 = new File(['content3'], 'test3.txt', { type: 'text/plain' });
416
- const mockFile4 = new File(['content4'], 'test4.txt', { type: 'text/plain' });
417
-
418
- Object.defineProperty(mockFile1, 'size', { value: 500 });
419
- Object.defineProperty(mockFile2, 'size', { value: 500 });
420
- Object.defineProperty(mockFile3, 'size', { value: 500 });
421
- Object.defineProperty(mockFile4, 'size', { value: 500 });
422
-
423
- const mockEvent = {
424
- target: {
425
- files: [mockFile1, mockFile2, mockFile3, mockFile4]
426
- }
427
- };
256
+ const { activeInput } = setupDOM(3, MB);
257
+ const files = [
258
+ makeMockFile('test1.txt', 500),
259
+ makeMockFile('test2.txt', 500),
260
+ makeMockFile('test3.txt', 500),
261
+ makeMockFile('test4.txt', 500),
262
+ ];
428
263
 
429
- eventHandlers.change(mockEvent);
264
+ triggerChange(activeInput, files);
430
265
 
431
- // Should only add 3 files (nbMaxFiles = 3)
432
266
  expect(setFilesListSpy).toHaveBeenCalledTimes(3);
433
267
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Maximum 3 fichiers autorisés.');
434
268
  });
435
269
 
436
270
  test('should reject file exceeding max size', () => {
437
- const mockFile = new File(['content'], 'large.txt', { type: 'text/plain' });
438
- Object.defineProperty(mockFile, 'size', { value: 2 * 1024 * 1024 }); // 2MB
439
- Object.defineProperty(mockFile, 'name', { value: 'large.txt' });
440
-
441
- const mockEvent = {
442
- target: {
443
- files: [mockFile]
444
- }
445
- };
271
+ const { activeInput } = setupDOM(3, MB);
272
+ const mockFile = makeMockFile('large.txt', 2 * MB);
446
273
 
447
- eventHandlers.change(mockEvent);
274
+ triggerChange(activeInput, [mockFile]);
448
275
 
449
276
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Le fichier large.txt dépasse la taille maximale.');
450
277
  expect(setFilesListSpy).not.toHaveBeenCalled();
451
278
  });
452
279
 
453
280
  test('should handle multiple valid files', () => {
454
- const mockFile1 = new File(['content1'], 'test1.txt', { type: 'text/plain' });
455
- const mockFile2 = new File(['content2'], 'test2.txt', { type: 'text/plain' });
456
-
457
- Object.defineProperty(mockFile1, 'size', { value: 500 });
458
- Object.defineProperty(mockFile2, 'size', { value: 600 });
459
-
460
- const mockEvent = {
461
- target: {
462
- files: [mockFile1, mockFile2]
463
- }
464
- };
281
+ const { activeInput } = setupDOM(3, MB);
282
+ const f1 = makeMockFile('test1.txt', 500);
283
+ const f2 = makeMockFile('test2.txt', 600);
465
284
 
466
- eventHandlers.change(mockEvent);
285
+ triggerChange(activeInput, [f1, f2]);
467
286
 
468
- // setFilesList est appelé avec la même référence d'array (comportement voulu)
469
287
  expect(setFilesListSpy).toHaveBeenCalledTimes(2);
470
- // À chaque appel, l'array contient tous les fichiers ajoutés jusqu'à présent
471
- const call1 = setFilesListSpy.mock.calls[0][0];
472
- const call2 = setFilesListSpy.mock.calls[1][0];
473
- expect(call1).toEqual(call2); // Même référence, contient les 2 fichiers
474
- expect(call2).toHaveLength(2);
475
- expect(call2).toContain(mockFile1);
476
- expect(call2).toContain(mockFile2);
288
+ const lastCall = setFilesListSpy.mock.calls[1][0];
289
+ expect(lastCall).toHaveLength(2);
290
+ expect(lastCall).toContain(f1);
291
+ expect(lastCall).toContain(f2);
477
292
  });
478
293
 
479
294
  test('should skip oversized file and continue with valid files', () => {
480
- const mockFile1 = new File(['content1'], 'small.txt', { type: 'text/plain' });
481
- const mockFile2 = new File(['content2'], 'large.txt', { type: 'text/plain' });
482
- const mockFile3 = new File(['content3'], 'small2.txt', { type: 'text/plain' });
483
-
484
- Object.defineProperty(mockFile1, 'size', { value: 500 });
485
- Object.defineProperty(mockFile2, 'size', { value: 2 * 1024 * 1024 });
486
- Object.defineProperty(mockFile2, 'name', { value: 'large.txt' });
487
- Object.defineProperty(mockFile3, 'size', { value: 600 });
488
-
489
- const mockEvent = {
490
- target: {
491
- files: [mockFile1, mockFile2, mockFile3]
492
- }
493
- };
295
+ const { activeInput } = setupDOM(3, MB);
296
+ const f1 = makeMockFile('small.txt', 500);
297
+ const f2 = makeMockFile('large.txt', 2 * MB);
298
+ const f3 = makeMockFile('small2.txt', 600);
494
299
 
495
- eventHandlers.change(mockEvent);
300
+ triggerChange(activeInput, [f1, f2, f3]);
496
301
 
497
- // setFilesList est appelé avec la même référence d'array (comportement voulu)
498
302
  expect(setFilesListSpy).toHaveBeenCalledTimes(2);
499
303
  const lastCall = setFilesListSpy.mock.calls[1][0];
500
304
  expect(lastCall).toHaveLength(2);
501
- expect(lastCall).toContain(mockFile1);
502
- expect(lastCall).toContain(mockFile3);
503
- expect(lastCall).not.toContain(mockFile2); // Le fichier trop gros n'est pas ajouté
305
+ expect(lastCall).toContain(f1);
306
+ expect(lastCall).toContain(f3);
307
+ expect(lastCall).not.toContain(f2);
504
308
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Le fichier large.txt dépasse la taille maximale.');
505
309
  });
506
310
  });
507
311
 
508
312
  describe('file preview rendering', () => {
509
- beforeEach(() => {
510
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
511
- });
512
-
513
313
  test('should render preview for non-image file', () => {
514
- const mockFile = new File(['content'], 'document.pdf', { type: 'application/pdf' });
515
- Object.defineProperty(mockFile, 'size', { value: 500 });
516
- Object.defineProperty(mockFile, 'name', { value: 'document.pdf' });
517
-
518
- const mockEvent = {
519
- target: {
520
- files: [mockFile]
521
- }
522
- };
314
+ const { activeInput, filesPreview } = setupDOM(3, MB);
315
+ const mockFile = makeMockFile('document.pdf', 500, 'application/pdf');
523
316
 
524
- eventHandlers.change(mockEvent);
317
+ triggerChange(activeInput, [mockFile]);
525
318
 
526
- expect(mockFilesPreview.append).toHaveBeenCalledWith(mockWrap);
527
- expect(mockFilesPreview.removeClass).toHaveBeenCalledWith('hide');
528
- expect(mockPreviewThumb.html).toHaveBeenCalledWith('<i class="fas fa-file fa-2x text-muted"></i>');
319
+ expect(filesPreview.querySelector('[data-file-id]')).not.toBeNull();
320
+ expect(filesPreview.classList.contains('hide')).toBe(false);
321
+ expect(filesPreview.querySelector('.preview-thumb').innerHTML).toContain('fas fa-file');
529
322
  });
530
323
 
531
324
  test('should render preview with image thumbnail for image file', (done) => {
532
- const mockFile = new File(['content'], 'photo.png', { type: 'image/png' });
533
- Object.defineProperty(mockFile, 'size', { value: 500 });
534
- Object.defineProperty(mockFile, 'name', { value: 'photo.png' });
535
- Object.defineProperty(mockFile, 'type', { value: 'image/png' });
536
-
537
- const mockEvent = {
538
- target: {
539
- files: [mockFile]
540
- }
541
- };
325
+ const { activeInput, filesPreview } = setupDOM(3, MB);
326
+ const mockFile = makeMockFile('photo.png', 500, 'image/png');
542
327
 
543
- eventHandlers.change(mockEvent);
328
+ triggerChange(activeInput, [mockFile]);
544
329
 
545
- // Wait for FileReader to process
546
330
  setTimeout(() => {
547
- expect(mockPreviewThumb.html).toHaveBeenCalledWith(
548
- expect.stringContaining('<img src="data:image/png;base64,')
549
- );
331
+ expect(filesPreview.querySelector('.preview-thumb').innerHTML).toContain('<img src="data:image/png;base64,');
550
332
  done();
551
333
  }, 10);
552
334
  });
553
335
 
554
336
  test('should generate unique file ID', () => {
555
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
556
- Object.defineProperty(mockFile, 'size', { value: 500 });
337
+ const { activeInput, filesPreview } = setupDOM(3, MB);
338
+ const mockFile = makeMockFile('test.txt', 500);
557
339
 
558
- const mockEvent = {
559
- target: {
560
- files: [mockFile]
561
- }
562
- };
340
+ triggerChange(activeInput, [mockFile]);
563
341
 
564
- eventHandlers.change(mockEvent);
565
-
566
- // Check that $ was called with a string containing data-file-id
567
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('data-file-id="f_'));
342
+ const wrap = filesPreview.querySelector('[data-file-id]');
343
+ expect(wrap).not.toBeNull();
344
+ expect(wrap.dataset.fileId).toMatch(/^f_/);
568
345
  });
569
346
 
570
347
  test('should setup close button handler', () => {
571
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
572
- Object.defineProperty(mockFile, 'size', { value: 500 });
573
-
574
- const mockEvent = {
575
- target: {
576
- files: [mockFile]
577
- }
578
- };
348
+ const { activeInput, filesPreview } = setupDOM(3, MB);
349
+ const mockFile = makeMockFile('test.txt', 500);
579
350
 
580
- eventHandlers.change(mockEvent);
351
+ triggerChange(activeInput, [mockFile]);
581
352
 
582
- expect(mockBtnClose.off).toHaveBeenCalledWith('click');
583
- expect(mockBtnClose.on).toHaveBeenCalledWith('click', expect.any(Function));
353
+ expect(filesPreview.querySelector('.btn-close')).not.toBeNull();
584
354
  });
585
355
  });
586
356
 
587
357
  describe('file removal', () => {
588
- let mockClosest;
589
- let removeButtonHandler;
590
-
591
- beforeEach(() => {
592
- mockClosest = {
593
- index: jest.fn(() => 0),
594
- remove: jest.fn()
595
- };
596
-
597
- // Reconfigure mockBtnClose to capture the remove handler
598
- mockBtnClose.on = jest.fn(function(event, handler) {
599
- if (event === 'click') {
600
- removeButtonHandler = handler;
601
- }
602
- return this;
603
- });
604
- mockBtnClose.closest = jest.fn(() => mockClosest);
605
-
606
- // Reconfigure mockWrap.closest to return our mockClosest
607
- mockWrap.closest = jest.fn(() => mockClosest);
608
-
609
- // Update global.$ to return proper closest
610
- global.$ = jest.fn((selector) => {
611
- if (typeof selector === 'string') {
612
- if (selector.includes('data-file-id')) {
613
- return mockWrap;
614
- }
615
- return {
616
- addClass: jest.fn().mockReturnThis(),
617
- removeClass: jest.fn().mockReturnThis()
618
- };
619
- }
620
- if (selector === mockBtnClose) {
621
- return {
622
- closest: jest.fn(() => mockClosest)
623
- };
624
- }
625
- return {};
626
- });
627
-
628
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
629
- });
630
-
631
358
  test('should remove file from list when close button clicked', () => {
632
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
633
- Object.defineProperty(mockFile, 'size', { value: 500 });
634
- Object.defineProperty(mockFile, 'name', { value: 'test.txt' });
635
-
636
- const mockEvent = {
637
- target: {
638
- files: [mockFile]
639
- }
640
- };
359
+ const { activeInput, filesPreview } = setupDOM(3, MB);
360
+ const mockFile = makeMockFile('test.txt', 500);
641
361
 
642
- // Add file
643
- eventHandlers.change(mockEvent);
362
+ triggerChange(activeInput, [mockFile]);
644
363
 
645
- // Click remove button
646
- removeButtonHandler.call(mockBtnClose);
364
+ const btnClose = filesPreview.querySelector('.btn-close');
365
+ btnClose.dispatchEvent(new Event('click'));
647
366
 
648
- // Should call setFilesList with empty array
649
367
  expect(setFilesListSpy).toHaveBeenLastCalledWith([]);
650
- expect(mockClosest.remove).toHaveBeenCalled();
368
+ expect(filesPreview.querySelector('[data-file-id]')).toBeNull();
651
369
  });
652
370
 
653
371
  test('should hide preview container when last file is removed', () => {
654
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
655
- Object.defineProperty(mockFile, 'size', { value: 500 });
656
- Object.defineProperty(mockFile, 'name', { value: 'test.txt' });
372
+ const { activeInput, filesPreview } = setupDOM(3, MB);
373
+ const mockFile = makeMockFile('test.txt', 500);
657
374
 
658
- const mockEvent = {
659
- target: {
660
- files: [mockFile]
661
- }
662
- };
663
-
664
- eventHandlers.change(mockEvent);
665
-
666
- // Reset mock calls
667
- mockFilesPreview.addClass.mockClear();
375
+ triggerChange(activeInput, [mockFile]);
668
376
 
669
- // Click remove button
670
- removeButtonHandler.call(mockBtnClose);
377
+ const btnClose = filesPreview.querySelector('.btn-close');
378
+ btnClose.dispatchEvent(new Event('click'));
671
379
 
672
- expect(mockFilesPreview.addClass).toHaveBeenCalledWith('hide');
380
+ expect(filesPreview.classList.contains('hide')).toBe(true);
673
381
  });
674
382
 
675
383
  test('should not hide preview container when files remain', () => {
676
- const mockFile1 = new File(['content1'], 'test1.txt', { type: 'text/plain' });
677
- const mockFile2 = new File(['content2'], 'test2.txt', { type: 'text/plain' });
384
+ const { activeInput, filesPreview } = setupDOM(3, MB);
385
+ const f1 = makeMockFile('test1.txt', 500);
386
+ const f2 = makeMockFile('test2.txt', 600);
678
387
 
679
- Object.defineProperty(mockFile1, 'size', { value: 500 });
680
- Object.defineProperty(mockFile1, 'name', { value: 'test1.txt' });
681
- Object.defineProperty(mockFile2, 'size', { value: 600 });
682
- Object.defineProperty(mockFile2, 'name', { value: 'test2.txt' });
683
-
684
- const mockEvent = {
685
- target: {
686
- files: [mockFile1, mockFile2]
687
- }
688
- };
689
-
690
- eventHandlers.change(mockEvent);
691
-
692
- // Reset mock calls
693
- mockFilesPreview.addClass.mockClear();
388
+ triggerChange(activeInput, [f1, f2]);
694
389
  setFilesListSpy.mockClear();
695
390
 
696
- // Click remove button for first file
697
- removeButtonHandler.call(mockBtnClose);
391
+ // Remove first file
392
+ const btnClose = filesPreview.querySelectorAll('.btn-close')[0];
393
+ btnClose.dispatchEvent(new Event('click'));
698
394
 
699
- // Should still have one file
700
- expect(setFilesListSpy).toHaveBeenLastCalledWith([mockFile2]);
701
- expect(mockFilesPreview.addClass).not.toHaveBeenCalledWith('hide');
395
+ expect(setFilesListSpy).toHaveBeenLastCalledWith([f2]);
396
+ expect(filesPreview.classList.contains('hide')).toBe(false);
702
397
  });
703
398
 
704
399
  test('should remove file by name and size reference', () => {
705
- const mockFile1 = new File(['content1'], 'test.txt', { type: 'text/plain' });
706
- const mockFile2 = new File(['content2'], 'test.txt', { type: 'text/plain' }); // Same name
707
- const mockFile3 = new File(['content3'], 'other.txt', { type: 'text/plain' });
708
-
709
- Object.defineProperty(mockFile1, 'size', { value: 500 });
710
- Object.defineProperty(mockFile1, 'name', { value: 'test.txt' });
711
- Object.defineProperty(mockFile2, 'size', { value: 600 }); // Different size
712
- Object.defineProperty(mockFile2, 'name', { value: 'test.txt' });
713
- Object.defineProperty(mockFile3, 'size', { value: 700 });
714
- Object.defineProperty(mockFile3, 'name', { value: 'other.txt' });
715
-
716
- const mockEvent = {
717
- target: {
718
- files: [mockFile1, mockFile2, mockFile3]
719
- }
720
- };
721
-
722
- eventHandlers.change(mockEvent);
400
+ const { activeInput, filesPreview } = setupDOM(3, MB);
401
+ const f1 = makeMockFile('test.txt', 500);
402
+ const f2 = makeMockFile('test.txt', 600); // same name, different size
403
+ const f3 = makeMockFile('other.txt', 700);
723
404
 
724
- // The files list should now contain all 3 files
725
- // When we remove, it should match by name AND size
405
+ triggerChange(activeInput, [f1, f2, f3]);
726
406
  setFilesListSpy.mockClear();
727
407
 
728
- removeButtonHandler.call(mockBtnClose);
408
+ // Remove the first wrap (corresponds to f1)
409
+ const btnClose = filesPreview.querySelectorAll('.btn-close')[0];
410
+ btnClose.dispatchEvent(new Event('click'));
729
411
 
730
- // Should remove only the file with matching name and size
731
- // Since mockClosest.index returns 0, it will try to remove mockFile1
732
412
  const lastCall = setFilesListSpy.mock.calls[setFilesListSpy.mock.calls.length - 1];
733
- expect(lastCall[0].length).toBe(2); // Should have 2 files remaining
413
+ expect(lastCall[0]).toHaveLength(2);
734
414
  });
735
415
  });
736
416
 
737
417
  describe('XSS protection', () => {
738
- beforeEach(() => {
739
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
740
- });
741
-
742
418
  test('should escape HTML tags in filename', () => {
743
- const mockFile = new File(['content'], '<script>alert("xss")</script>.txt', { type: 'text/plain' });
744
- Object.defineProperty(mockFile, 'size', { value: 500 });
745
- Object.defineProperty(mockFile, 'name', { value: '<script>alert("xss")</script>.txt' });
419
+ const { activeInput, filesPreview } = setupDOM(3, MB);
420
+ const mockFile = makeMockFile('<script>alert("xss")</script>.txt', 500);
746
421
 
747
- const mockEvent = {
748
- target: {
749
- files: [mockFile]
750
- }
751
- };
422
+ triggerChange(activeInput, [mockFile]);
752
423
 
753
- eventHandlers.change(mockEvent);
754
-
755
- // Vérifie que les balises HTML sont échappées
756
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('&lt;script&gt;'));
757
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('&lt;&#x2F;script&gt;'));
758
- expect(global.$).not.toHaveBeenCalledWith(expect.stringContaining('<script>'));
424
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
425
+ expect(nameDiv.innerHTML).toContain('&lt;script&gt;');
426
+ expect(nameDiv.innerHTML).not.toContain('<script>');
759
427
  });
760
428
 
761
429
  test('should escape quotes in filename', () => {
762
- const mockFile = new File(['content'], 'file"with\'quotes.txt', { type: 'text/plain' });
763
- Object.defineProperty(mockFile, 'size', { value: 500 });
764
- Object.defineProperty(mockFile, 'name', { value: 'file"with\'quotes.txt' });
765
-
766
- const mockEvent = {
767
- target: {
768
- files: [mockFile]
769
- }
770
- };
430
+ const { activeInput, filesPreview } = setupDOM(3, MB);
431
+ const mockFile = makeMockFile('file"with\'quotes.txt', 500);
771
432
 
772
- eventHandlers.change(mockEvent);
433
+ triggerChange(activeInput, [mockFile]);
773
434
 
774
- // Vérifie que les guillemets sont échappés
775
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('&quot;'));
776
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('&#39;'));
435
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
436
+ expect(nameDiv.textContent).toContain('"');
437
+ expect(nameDiv.textContent).toContain("'");
777
438
  });
778
439
 
779
440
  test('should escape ampersand in filename', () => {
780
- const mockFile = new File(['content'], 'file&name.txt', { type: 'text/plain' });
781
- Object.defineProperty(mockFile, 'size', { value: 500 });
782
- Object.defineProperty(mockFile, 'name', { value: 'file&name.txt' });
783
-
784
- const mockEvent = {
785
- target: {
786
- files: [mockFile]
787
- }
788
- };
441
+ const { activeInput, filesPreview } = setupDOM(3, MB);
442
+ const mockFile = makeMockFile('file&name.txt', 500);
789
443
 
790
- eventHandlers.change(mockEvent);
444
+ triggerChange(activeInput, [mockFile]);
791
445
 
792
- // Vérifie que & est échappé en &amp;
793
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('file&amp;name.txt'));
794
- expect(global.$).not.toHaveBeenCalledWith(expect.stringMatching(/file&name\.txt/));
446
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
447
+ expect(nameDiv.innerHTML).toContain('&amp;');
448
+ expect(nameDiv.innerHTML).not.toMatch(/file&name\.txt/);
795
449
  });
796
450
 
797
451
  test('should handle filename with all dangerous characters', () => {
798
- const mockFile = new File(['content'], '<>"&\'/test.txt', { type: 'text/plain' });
799
- Object.defineProperty(mockFile, 'size', { value: 500 });
800
- Object.defineProperty(mockFile, 'name', { value: '<>"&\'/test.txt' });
801
-
802
- const mockEvent = {
803
- target: {
804
- files: [mockFile]
805
- }
806
- };
452
+ const { activeInput, filesPreview } = setupDOM(3, MB);
453
+ const mockFile = makeMockFile('<>"&\'/test.txt', 500);
807
454
 
808
- eventHandlers.change(mockEvent);
455
+ triggerChange(activeInput, [mockFile]);
809
456
 
810
- // Vérifie que tous les caractères dangereux sont échappés
811
- const calls = global.$.mock.calls;
812
- const htmlCall = calls.find(call => typeof call[0] === 'string' && call[0].includes('data-file-id'));
813
- expect(htmlCall).toBeDefined();
814
- expect(htmlCall[0]).toContain('&lt;');
815
- expect(htmlCall[0]).toContain('&gt;');
816
- expect(htmlCall[0]).toContain('&quot;');
817
- expect(htmlCall[0]).toContain('&amp;');
818
- expect(htmlCall[0]).toContain('&#39;');
819
- expect(htmlCall[0]).toContain('&#x2F;');
457
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
458
+ expect(nameDiv.innerHTML).toContain('&lt;');
459
+ expect(nameDiv.innerHTML).toContain('&gt;');
460
+ expect(nameDiv.textContent).toContain('"');
461
+ expect(nameDiv.innerHTML).toContain('&amp;');
462
+ expect(nameDiv.textContent).toContain("'");
463
+ expect(nameDiv.textContent).toContain('/');
820
464
  });
821
465
  });
822
466
 
823
467
  describe('edge cases', () => {
824
- beforeEach(() => {
825
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
826
- });
827
-
828
468
  test('should handle empty file selection', () => {
829
- const mockEvent = {
830
- target: {
831
- files: []
832
- }
833
- };
469
+ const { activeInput } = setupDOM(3, MB);
834
470
 
835
- eventHandlers.change(mockEvent);
471
+ triggerChange(activeInput, []);
836
472
 
837
473
  expect(setFilesListSpy).not.toHaveBeenCalled();
838
- expect(mockFileInput.val).toHaveBeenCalledWith('');
839
474
  });
840
475
 
841
476
  test('should handle file with zero size', () => {
842
- const mockFile = new File([''], 'empty.txt', { type: 'text/plain' });
843
- Object.defineProperty(mockFile, 'size', { value: 0 });
844
-
845
- const mockEvent = {
846
- target: {
847
- files: [mockFile]
848
- }
849
- };
477
+ const { activeInput } = setupDOM(3, MB);
478
+ const mockFile = makeMockFile('empty.txt', 0);
850
479
 
851
- eventHandlers.change(mockEvent);
480
+ triggerChange(activeInput, [mockFile]);
852
481
 
853
- // File with size 0 should still be accepted
854
482
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
855
483
  });
856
484
 
857
485
  test('should handle file at exact max size', () => {
858
- const mockFile = new File(['content'], 'exact.txt', { type: 'text/plain' });
859
- Object.defineProperty(mockFile, 'size', { value: 1024 * 1024 }); // Exactly 1MB
486
+ const { activeInput } = setupDOM(3, MB);
487
+ const mockFile = makeMockFile('exact.txt', MB);
860
488
 
861
- const mockEvent = {
862
- target: {
863
- files: [mockFile]
864
- }
865
- };
866
-
867
- eventHandlers.change(mockEvent);
489
+ triggerChange(activeInput, [mockFile]);
868
490
 
869
- // File at exact max size should be accepted
870
491
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
871
492
  expect(FlashMessage.displayError).not.toHaveBeenCalled();
872
493
  });
873
494
 
874
495
  test('should handle file one byte over max size', () => {
875
- const mockFile = new File(['content'], 'oversize.txt', { type: 'text/plain' });
876
- Object.defineProperty(mockFile, 'size', { value: 1024 * 1024 + 1 }); // 1MB + 1 byte
877
- Object.defineProperty(mockFile, 'name', { value: 'oversize.txt' });
878
-
879
- const mockEvent = {
880
- target: {
881
- files: [mockFile]
882
- }
883
- };
496
+ const { activeInput } = setupDOM(3, MB);
497
+ const mockFile = makeMockFile('oversize.txt', MB + 1);
884
498
 
885
- eventHandlers.change(mockEvent);
499
+ triggerChange(activeInput, [mockFile]);
886
500
 
887
501
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Le fichier oversize.txt dépasse la taille maximale.');
888
502
  expect(setFilesListSpy).not.toHaveBeenCalled();
889
503
  });
890
504
 
891
505
  test('should handle special characters in filename', () => {
892
- const mockFile = new File(['content'], 'file with spaces & special#chars.txt', { type: 'text/plain' });
893
- Object.defineProperty(mockFile, 'size', { value: 500 });
894
- Object.defineProperty(mockFile, 'name', { value: 'file with spaces & special#chars.txt' });
506
+ const { activeInput, filesPreview } = setupDOM(3, MB);
507
+ const mockFile = makeMockFile('file with spaces & special#chars.txt', 500);
895
508
 
896
- const mockEvent = {
897
- target: {
898
- files: [mockFile]
899
- }
900
- };
901
-
902
- eventHandlers.change(mockEvent);
509
+ triggerChange(activeInput, [mockFile]);
903
510
 
904
511
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
905
- // Vérifie que le caractère & est échappé en &amp; pour éviter les failles XSS
906
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('file with spaces &amp; special#chars.txt'));
512
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
513
+ expect(nameDiv.innerHTML).toContain('&amp;');
907
514
  });
908
515
 
909
516
  test('should handle various image types', () => {
910
517
  const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
911
518
 
912
- imageTypes.forEach((type, index) => {
913
- // Réinitialiser pour chaque test d'image
914
- if (index > 0) {
915
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 3, 1024 * 1024);
916
- }
917
- setFilesListSpy.mockClear();
918
-
919
- const mockFile = new File(['content'], `photo.${type.split('/')[1]}`, { type });
920
- Object.defineProperty(mockFile, 'size', { value: 500 });
921
- Object.defineProperty(mockFile, 'type', { value: type });
922
-
923
- const mockEvent = {
924
- target: {
925
- files: [mockFile]
926
- }
927
- };
928
-
929
- eventHandlers.change(mockEvent);
930
-
931
- expect(setFilesListSpy).toHaveBeenCalled();
519
+ imageTypes.forEach((type) => {
520
+ document.body.innerHTML = `
521
+ <div class="form-group">
522
+ <input type="file" id="fileInput" />
523
+ </div>
524
+ `;
525
+ const fileInput = document.getElementById('fileInput');
526
+ const formGroup = document.querySelector('.form-group');
527
+ const spy = jest.fn();
528
+ MultiFilesInput.init(fileInput, spy, 3, MB);
529
+ const activeInput = formGroup.querySelector('input[type="file"]');
530
+
531
+ const mockFile = makeMockFile(`photo.${type.split('/')[1]}`, 500, type);
532
+ triggerChange(activeInput, [mockFile]);
533
+
534
+ expect(spy).toHaveBeenCalled();
535
+ spy.mockClear();
932
536
  });
933
537
  });
934
538
 
935
539
  test('should handle max files limit of 0', () => {
936
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 0, 1024 * 1024);
937
-
938
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
939
- Object.defineProperty(mockFile, 'size', { value: 500 });
940
-
941
- const mockEvent = {
942
- target: {
943
- files: [mockFile]
944
- }
945
- };
540
+ const { activeInput } = setupDOM(0, MB);
541
+ const mockFile = makeMockFile('test.txt', 500);
946
542
 
947
- eventHandlers.change(mockEvent);
543
+ triggerChange(activeInput, [mockFile]);
948
544
 
949
545
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Maximum 0 fichiers autorisés.');
950
546
  expect(setFilesListSpy).not.toHaveBeenCalled();
951
547
  });
952
548
 
953
549
  test('should handle max files limit of 1', () => {
954
- MultiFilesInput.init(mockFileInput, setFilesListSpy, 1, 1024 * 1024);
955
-
956
- const mockFile1 = new File(['content1'], 'test1.txt', { type: 'text/plain' });
957
- const mockFile2 = new File(['content2'], 'test2.txt', { type: 'text/plain' });
958
-
959
- Object.defineProperty(mockFile1, 'size', { value: 500 });
960
- Object.defineProperty(mockFile2, 'size', { value: 600 });
961
-
962
- const mockEvent = {
963
- target: {
964
- files: [mockFile1, mockFile2]
965
- }
966
- };
550
+ const { activeInput } = setupDOM(1, MB);
551
+ const f1 = makeMockFile('test1.txt', 500);
552
+ const f2 = makeMockFile('test2.txt', 600);
967
553
 
968
- eventHandlers.change(mockEvent);
554
+ triggerChange(activeInput, [f1, f2]);
969
555
 
970
556
  expect(setFilesListSpy).toHaveBeenCalledTimes(1);
971
- expect(setFilesListSpy).toHaveBeenCalledWith([mockFile1]);
557
+ expect(setFilesListSpy).toHaveBeenCalledWith([f1]);
972
558
  expect(FlashMessage.displayError).toHaveBeenCalledWith('Maximum 1 fichiers autorisés.');
973
559
  });
974
560
 
975
561
  test('should handle file with null name', () => {
976
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
977
- Object.defineProperty(mockFile, 'size', { value: 500 });
978
- Object.defineProperty(mockFile, 'name', { value: null });
562
+ const { activeInput, filesPreview } = setupDOM(3, MB);
563
+ const mockFile = makeMockFile(null, 500);
979
564
 
980
- const mockEvent = {
981
- target: {
982
- files: [mockFile]
983
- }
984
- };
985
-
986
- eventHandlers.change(mockEvent);
565
+ triggerChange(activeInput, [mockFile]);
987
566
 
988
- // Le fichier devrait être ajouté même avec un nom null
989
567
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
990
- // Vérifie que le nom est traité comme une chaîne vide
991
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('max-width:160px;"></div>'));
568
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
569
+ expect(nameDiv).not.toBeNull();
992
570
  });
993
571
 
994
572
  test('should handle file with undefined name', () => {
995
- const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' });
996
- Object.defineProperty(mockFile, 'size', { value: 500 });
997
- Object.defineProperty(mockFile, 'name', { value: undefined });
998
-
999
- const mockEvent = {
1000
- target: {
1001
- files: [mockFile]
1002
- }
1003
- };
573
+ const { activeInput, filesPreview } = setupDOM(3, MB);
574
+ const mockFile = makeMockFile(undefined, 500);
1004
575
 
1005
- eventHandlers.change(mockEvent);
576
+ triggerChange(activeInput, [mockFile]);
1006
577
 
1007
- // Le fichier devrait être ajouté même avec un nom undefined
1008
578
  expect(setFilesListSpy).toHaveBeenCalledWith([mockFile]);
1009
- // Vérifie que le nom est traité comme une chaîne vide
1010
- expect(global.$).toHaveBeenCalledWith(expect.stringContaining('max-width:160px;"></div>'));
579
+ const nameDiv = filesPreview.querySelector('.small.text-truncate');
580
+ expect(nameDiv).not.toBeNull();
1011
581
  });
1012
582
  });
1013
583
  });