@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.
- package/details_sub_array.js +45 -41
- package/form_helper.js +283 -232
- package/google_charts.js +154 -144
- package/google_maps.js +1 -1
- package/import_from_csv.js +166 -157
- package/multi_files_input.js +42 -34
- package/multiple_action_in_table.js +115 -105
- package/package.json +1 -1
- package/paging.js +103 -84
- package/select_all.js +65 -70
- package/sortable_list.js +12 -13
- package/tests/details_sub_array.test.js +211 -239
- package/tests/form_helper.test.js +553 -673
- package/tests/google_charts.test.js +338 -339
- package/tests/google_maps.test.js +3 -15
- package/tests/import_from_csv.test.js +391 -652
- package/tests/multi_files_input.test.js +292 -722
- package/tests/multiple_action_in_table.test.js +439 -417
- package/tests/paging.test.js +344 -475
- package/tests/select_all.test.js +232 -318
- package/tests/sortable_list.test.js +176 -500
- package/tests/user.test.js +163 -54
- package/user.js +35 -38
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
expect(
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
expect(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
179
|
+
const { activeInput } = setupDOM();
|
|
180
|
+
const mockFile = makeMockFile('test.txt', 500);
|
|
269
181
|
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
201
|
+
const dropzone = formGroup.querySelector('.multi_files_input_dropzone');
|
|
202
|
+
dropzone.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
287
203
|
|
|
288
|
-
expect(
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
339
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
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
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
398
|
-
|
|
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
|
-
|
|
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
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
455
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
expect(
|
|
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
|
|
481
|
-
const
|
|
482
|
-
const
|
|
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
|
-
|
|
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(
|
|
502
|
-
expect(lastCall).toContain(
|
|
503
|
-
expect(lastCall).not.toContain(
|
|
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
|
|
515
|
-
|
|
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
|
-
|
|
317
|
+
triggerChange(activeInput, [mockFile]);
|
|
525
318
|
|
|
526
|
-
expect(
|
|
527
|
-
expect(
|
|
528
|
-
expect(
|
|
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
|
|
533
|
-
|
|
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
|
-
|
|
328
|
+
triggerChange(activeInput, [mockFile]);
|
|
544
329
|
|
|
545
|
-
// Wait for FileReader to process
|
|
546
330
|
setTimeout(() => {
|
|
547
|
-
expect(
|
|
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
|
|
556
|
-
|
|
337
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
338
|
+
const mockFile = makeMockFile('test.txt', 500);
|
|
557
339
|
|
|
558
|
-
|
|
559
|
-
target: {
|
|
560
|
-
files: [mockFile]
|
|
561
|
-
}
|
|
562
|
-
};
|
|
340
|
+
triggerChange(activeInput, [mockFile]);
|
|
563
341
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
572
|
-
|
|
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
|
-
|
|
351
|
+
triggerChange(activeInput, [mockFile]);
|
|
581
352
|
|
|
582
|
-
expect(
|
|
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
|
|
633
|
-
|
|
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
|
-
|
|
643
|
-
eventHandlers.change(mockEvent);
|
|
362
|
+
triggerChange(activeInput, [mockFile]);
|
|
644
363
|
|
|
645
|
-
|
|
646
|
-
|
|
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(
|
|
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
|
|
655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
377
|
+
const btnClose = filesPreview.querySelector('.btn-close');
|
|
378
|
+
btnClose.dispatchEvent(new Event('click'));
|
|
671
379
|
|
|
672
|
-
expect(
|
|
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
|
|
677
|
-
const
|
|
384
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
385
|
+
const f1 = makeMockFile('test1.txt', 500);
|
|
386
|
+
const f2 = makeMockFile('test2.txt', 600);
|
|
678
387
|
|
|
679
|
-
|
|
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
|
-
//
|
|
697
|
-
|
|
391
|
+
// Remove first file
|
|
392
|
+
const btnClose = filesPreview.querySelectorAll('.btn-close')[0];
|
|
393
|
+
btnClose.dispatchEvent(new Event('click'));
|
|
698
394
|
|
|
699
|
-
|
|
700
|
-
expect(
|
|
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
|
|
706
|
-
const
|
|
707
|
-
const
|
|
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
|
-
|
|
725
|
-
// When we remove, it should match by name AND size
|
|
405
|
+
triggerChange(activeInput, [f1, f2, f3]);
|
|
726
406
|
setFilesListSpy.mockClear();
|
|
727
407
|
|
|
728
|
-
|
|
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]
|
|
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
|
|
744
|
-
|
|
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
|
-
|
|
748
|
-
target: {
|
|
749
|
-
files: [mockFile]
|
|
750
|
-
}
|
|
751
|
-
};
|
|
422
|
+
triggerChange(activeInput, [mockFile]);
|
|
752
423
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
expect(global.$).toHaveBeenCalledWith(expect.stringContaining('<script>'));
|
|
757
|
-
expect(global.$).toHaveBeenCalledWith(expect.stringContaining('</script>'));
|
|
758
|
-
expect(global.$).not.toHaveBeenCalledWith(expect.stringContaining('<script>'));
|
|
424
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
425
|
+
expect(nameDiv.innerHTML).toContain('<script>');
|
|
426
|
+
expect(nameDiv.innerHTML).not.toContain('<script>');
|
|
759
427
|
});
|
|
760
428
|
|
|
761
429
|
test('should escape quotes in filename', () => {
|
|
762
|
-
const
|
|
763
|
-
|
|
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
|
-
|
|
433
|
+
triggerChange(activeInput, [mockFile]);
|
|
773
434
|
|
|
774
|
-
|
|
775
|
-
expect(
|
|
776
|
-
expect(
|
|
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
|
|
781
|
-
|
|
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
|
-
|
|
444
|
+
triggerChange(activeInput, [mockFile]);
|
|
791
445
|
|
|
792
|
-
|
|
793
|
-
expect(
|
|
794
|
-
expect(
|
|
446
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
447
|
+
expect(nameDiv.innerHTML).toContain('&');
|
|
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
|
|
799
|
-
|
|
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
|
-
|
|
455
|
+
triggerChange(activeInput, [mockFile]);
|
|
809
456
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
expect(
|
|
814
|
-
expect(
|
|
815
|
-
expect(
|
|
816
|
-
expect(
|
|
817
|
-
expect(htmlCall[0]).toContain('&');
|
|
818
|
-
expect(htmlCall[0]).toContain(''');
|
|
819
|
-
expect(htmlCall[0]).toContain('/');
|
|
457
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
458
|
+
expect(nameDiv.innerHTML).toContain('<');
|
|
459
|
+
expect(nameDiv.innerHTML).toContain('>');
|
|
460
|
+
expect(nameDiv.textContent).toContain('"');
|
|
461
|
+
expect(nameDiv.innerHTML).toContain('&');
|
|
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
|
|
830
|
-
target: {
|
|
831
|
-
files: []
|
|
832
|
-
}
|
|
833
|
-
};
|
|
469
|
+
const { activeInput } = setupDOM(3, MB);
|
|
834
470
|
|
|
835
|
-
|
|
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
|
|
843
|
-
|
|
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
|
-
|
|
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
|
|
859
|
-
|
|
486
|
+
const { activeInput } = setupDOM(3, MB);
|
|
487
|
+
const mockFile = makeMockFile('exact.txt', MB);
|
|
860
488
|
|
|
861
|
-
|
|
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
|
|
876
|
-
|
|
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
|
-
|
|
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
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
906
|
-
expect(
|
|
512
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
513
|
+
expect(nameDiv.innerHTML).toContain('&');
|
|
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
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
const
|
|
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
|
-
|
|
554
|
+
triggerChange(activeInput, [f1, f2]);
|
|
969
555
|
|
|
970
556
|
expect(setFilesListSpy).toHaveBeenCalledTimes(1);
|
|
971
|
-
expect(setFilesListSpy).toHaveBeenCalledWith([
|
|
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
|
|
977
|
-
|
|
978
|
-
Object.defineProperty(mockFile, 'name', { value: null });
|
|
562
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
563
|
+
const mockFile = makeMockFile(null, 500);
|
|
979
564
|
|
|
980
|
-
|
|
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
|
-
|
|
991
|
-
expect(
|
|
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
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1010
|
-
expect(
|
|
579
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
580
|
+
expect(nameDiv).not.toBeNull();
|
|
1011
581
|
});
|
|
1012
582
|
});
|
|
1013
583
|
});
|