@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.
- package/count_down.js +46 -48
- package/date_time.js +0 -1
- package/details_sub_array.js +65 -50
- package/flash_message.js +10 -6
- package/form_date.js +144 -153
- package/form_helper.js +283 -232
- package/google_charts.js +154 -144
- package/google_maps.js +1 -1
- package/import_from_csv.js +198 -160
- package/multi_files_input.js +44 -35
- package/multiple_action_in_table.js +123 -109
- package/package.json +1 -1
- package/paging.js +103 -84
- package/select_all.js +65 -70
- package/sortable_list.js +12 -13
- package/tests/count_down.test.js +131 -352
- package/tests/details_sub_array.test.js +213 -258
- package/tests/flash_message.test.js +21 -153
- package/tests/form_date.test.js +287 -961
- 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 +421 -640
- package/tests/multi_files_input.test.js +305 -737
- package/tests/multiple_action_in_table.test.js +442 -429
- package/tests/open_street_map.test.js +15 -23
- 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
|
@@ -1,17 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
1
4
|
const { MultiFilesInput } = require('../multi_files_input');
|
|
2
|
-
const {
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
expect(
|
|
193
|
-
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');
|
|
194
86
|
});
|
|
195
87
|
|
|
196
88
|
test('should not create dropzone when it already exists', () => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
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);
|
|
207
99
|
});
|
|
208
100
|
|
|
209
101
|
test('should create files preview container when it does not exist', () => {
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
expect(
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
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);
|
|
260
165
|
});
|
|
261
166
|
|
|
262
167
|
test('should setup drop handler on dropzone', () => {
|
|
263
|
-
|
|
168
|
+
const { dropzone } = setupDOM();
|
|
169
|
+
const mockFile = makeMockFile('test.txt', 500);
|
|
264
170
|
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
179
|
+
const { activeInput } = setupDOM();
|
|
180
|
+
const mockFile = makeMockFile('test.txt', 500);
|
|
181
|
+
|
|
182
|
+
triggerChange(activeInput, [mockFile]);
|
|
271
183
|
|
|
272
|
-
expect(
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
201
|
+
const dropzone = formGroup.querySelector('.multi_files_input_dropzone');
|
|
202
|
+
dropzone.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
289
203
|
|
|
290
|
-
expect(
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
341
|
-
|
|
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
|
-
|
|
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
|
|
371
|
-
preventDefault: jest.fn(),
|
|
372
|
-
stopPropagation: jest.fn(),
|
|
373
|
-
originalEvent: {}
|
|
374
|
-
};
|
|
232
|
+
const { dropzone } = setupDOM();
|
|
375
233
|
|
|
376
|
-
const
|
|
377
|
-
|
|
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
|
-
|
|
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
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
264
|
+
triggerChange(activeInput, files);
|
|
432
265
|
|
|
433
|
-
// Should only add 3 files (nbMaxFiles = 3)
|
|
434
266
|
expect(setFilesListSpy).toHaveBeenCalledTimes(3);
|
|
435
|
-
expect(
|
|
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
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
target: {
|
|
445
|
-
files: [mockFile]
|
|
446
|
-
}
|
|
447
|
-
};
|
|
274
|
+
triggerChange(activeInput, [mockFile]);
|
|
448
275
|
|
|
449
|
-
|
|
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
|
|
457
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
expect(
|
|
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
|
|
483
|
-
const
|
|
484
|
-
const
|
|
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
|
-
|
|
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(
|
|
504
|
-
expect(lastCall).toContain(
|
|
505
|
-
expect(lastCall).not.toContain(
|
|
506
|
-
expect(
|
|
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
|
|
517
|
-
|
|
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
|
-
|
|
521
|
-
target: {
|
|
522
|
-
files: [mockFile]
|
|
523
|
-
}
|
|
524
|
-
};
|
|
317
|
+
triggerChange(activeInput, [mockFile]);
|
|
525
318
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
expect(
|
|
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
|
|
535
|
-
|
|
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
|
-
|
|
328
|
+
triggerChange(activeInput, [mockFile]);
|
|
546
329
|
|
|
547
|
-
// Wait for FileReader to process
|
|
548
330
|
setTimeout(() => {
|
|
549
|
-
expect(
|
|
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
|
|
558
|
-
|
|
337
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
338
|
+
const mockFile = makeMockFile('test.txt', 500);
|
|
559
339
|
|
|
560
|
-
|
|
561
|
-
target: {
|
|
562
|
-
files: [mockFile]
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
eventHandlers.change(mockEvent);
|
|
340
|
+
triggerChange(activeInput, [mockFile]);
|
|
567
341
|
|
|
568
|
-
|
|
569
|
-
expect(
|
|
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
|
|
574
|
-
|
|
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
|
-
|
|
351
|
+
triggerChange(activeInput, [mockFile]);
|
|
583
352
|
|
|
584
|
-
expect(
|
|
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
|
|
635
|
-
|
|
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
|
-
|
|
645
|
-
eventHandlers.change(mockEvent);
|
|
362
|
+
triggerChange(activeInput, [mockFile]);
|
|
646
363
|
|
|
647
|
-
|
|
648
|
-
|
|
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(
|
|
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
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
377
|
+
const btnClose = filesPreview.querySelector('.btn-close');
|
|
378
|
+
btnClose.dispatchEvent(new Event('click'));
|
|
673
379
|
|
|
674
|
-
expect(
|
|
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
|
|
679
|
-
const
|
|
384
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
385
|
+
const f1 = makeMockFile('test1.txt', 500);
|
|
386
|
+
const f2 = makeMockFile('test2.txt', 600);
|
|
680
387
|
|
|
681
|
-
|
|
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
|
-
//
|
|
699
|
-
|
|
391
|
+
// Remove first file
|
|
392
|
+
const btnClose = filesPreview.querySelectorAll('.btn-close')[0];
|
|
393
|
+
btnClose.dispatchEvent(new Event('click'));
|
|
700
394
|
|
|
701
|
-
|
|
702
|
-
expect(
|
|
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
|
|
708
|
-
const
|
|
709
|
-
const
|
|
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
|
-
|
|
727
|
-
// When we remove, it should match by name AND size
|
|
405
|
+
triggerChange(activeInput, [f1, f2, f3]);
|
|
728
406
|
setFilesListSpy.mockClear();
|
|
729
407
|
|
|
730
|
-
|
|
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]
|
|
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
|
|
746
|
-
|
|
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
|
-
|
|
750
|
-
target: {
|
|
751
|
-
files: [mockFile]
|
|
752
|
-
}
|
|
753
|
-
};
|
|
422
|
+
triggerChange(activeInput, [mockFile]);
|
|
754
423
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
expect(global.$).toHaveBeenCalledWith(expect.stringContaining('<script>'));
|
|
759
|
-
expect(global.$).toHaveBeenCalledWith(expect.stringContaining('</script>'));
|
|
760
|
-
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>');
|
|
761
427
|
});
|
|
762
428
|
|
|
763
429
|
test('should escape quotes in filename', () => {
|
|
764
|
-
const
|
|
765
|
-
|
|
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
|
-
|
|
433
|
+
triggerChange(activeInput, [mockFile]);
|
|
775
434
|
|
|
776
|
-
|
|
777
|
-
expect(
|
|
778
|
-
expect(
|
|
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
|
|
783
|
-
|
|
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
|
-
|
|
444
|
+
triggerChange(activeInput, [mockFile]);
|
|
793
445
|
|
|
794
|
-
|
|
795
|
-
expect(
|
|
796
|
-
expect(
|
|
446
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
447
|
+
expect(nameDiv.innerHTML).toContain('&');
|
|
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
|
|
801
|
-
|
|
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
|
-
|
|
455
|
+
triggerChange(activeInput, [mockFile]);
|
|
811
456
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
expect(
|
|
816
|
-
expect(
|
|
817
|
-
expect(
|
|
818
|
-
expect(
|
|
819
|
-
expect(htmlCall[0]).toContain('&');
|
|
820
|
-
expect(htmlCall[0]).toContain(''');
|
|
821
|
-
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('/');
|
|
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
|
|
832
|
-
target: {
|
|
833
|
-
files: []
|
|
834
|
-
}
|
|
835
|
-
};
|
|
469
|
+
const { activeInput } = setupDOM(3, MB);
|
|
836
470
|
|
|
837
|
-
|
|
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
|
|
845
|
-
|
|
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
|
-
|
|
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
|
|
861
|
-
|
|
486
|
+
const { activeInput } = setupDOM(3, MB);
|
|
487
|
+
const mockFile = makeMockFile('exact.txt', MB);
|
|
862
488
|
|
|
863
|
-
|
|
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(
|
|
492
|
+
expect(FlashMessage.displayError).not.toHaveBeenCalled();
|
|
874
493
|
});
|
|
875
494
|
|
|
876
495
|
test('should handle file one byte over max size', () => {
|
|
877
|
-
const
|
|
878
|
-
|
|
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
|
-
|
|
499
|
+
triggerChange(activeInput, [mockFile]);
|
|
888
500
|
|
|
889
|
-
expect(
|
|
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
|
|
895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
908
|
-
expect(
|
|
512
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
513
|
+
expect(nameDiv.innerHTML).toContain('&');
|
|
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
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
+
triggerChange(activeInput, [mockFile]);
|
|
950
544
|
|
|
951
|
-
expect(
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
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
|
-
|
|
554
|
+
triggerChange(activeInput, [f1, f2]);
|
|
971
555
|
|
|
972
556
|
expect(setFilesListSpy).toHaveBeenCalledTimes(1);
|
|
973
|
-
expect(setFilesListSpy).toHaveBeenCalledWith([
|
|
974
|
-
expect(
|
|
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
|
|
979
|
-
|
|
980
|
-
Object.defineProperty(mockFile, 'name', { value: null });
|
|
562
|
+
const { activeInput, filesPreview } = setupDOM(3, MB);
|
|
563
|
+
const mockFile = makeMockFile(null, 500);
|
|
981
564
|
|
|
982
|
-
|
|
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
|
-
|
|
993
|
-
expect(
|
|
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
|
|
998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
expect(
|
|
579
|
+
const nameDiv = filesPreview.querySelector('.small.text-truncate');
|
|
580
|
+
expect(nameDiv).not.toBeNull();
|
|
1013
581
|
});
|
|
1014
582
|
});
|
|
1015
583
|
});
|