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