@osimatic/helpers-js 1.5.21 → 1.5.23
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/chartjs.js +4 -1
- package/form_helper.js +4 -3
- package/media.js +1 -1
- package/package.json +1 -1
- package/select_all.js +27 -0
- package/tests/chartjs.test.js +174 -147
- package/tests/form_helper.test.js +323 -10
- package/tests/select_all.test.js +48 -0
package/chartjs.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const { toEl } = require('./util');
|
|
2
2
|
const deepmerge = require('deepmerge');
|
|
3
|
+
const { DatePeriod } = require('./date_time');
|
|
3
4
|
|
|
4
5
|
class Chartjs {
|
|
5
6
|
static init() {
|
|
6
|
-
if (
|
|
7
|
+
if (Chartjs.initialized) {
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -40,6 +41,7 @@ class Chartjs {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
static createStackedChart(chartDiv, chartData, title=null, options={}) {
|
|
44
|
+
Chartjs.init();
|
|
43
45
|
chartDiv = toEl(chartDiv);
|
|
44
46
|
chartDiv.innerHTML = '';
|
|
45
47
|
new Chart(chartDiv.getContext("2d"), deepmerge({
|
|
@@ -90,6 +92,7 @@ class Chartjs {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
static createBarChart(chartDiv, chartData, title=null, options={}) {
|
|
95
|
+
Chartjs.init();
|
|
93
96
|
chartDiv = toEl(chartDiv);
|
|
94
97
|
chartDiv.innerHTML = '';
|
|
95
98
|
new Chart(chartDiv.getContext("2d"), deepmerge({
|
package/form_helper.js
CHANGED
|
@@ -239,7 +239,7 @@ class FormHelper {
|
|
|
239
239
|
return json.error;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
if (onlyIfUniqueError && !json.length || json.length > 1) {
|
|
242
|
+
if (onlyIfUniqueError && (!json.length || json.length > 1)) {
|
|
243
243
|
return null;
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -263,7 +263,7 @@ class FormHelper {
|
|
|
263
263
|
return json.error;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
if (onlyIfUniqueError && !json.length || json.length > 1) {
|
|
266
|
+
if (onlyIfUniqueError && (!json.length || json.length > 1)) {
|
|
267
267
|
return null;
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -279,9 +279,10 @@ class FormHelper {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
static hideFormErrors(form) {
|
|
282
|
+
const wasJQuery = form && form.jquery;
|
|
282
283
|
form = toEl(form);
|
|
283
284
|
form.querySelectorAll('div.form_errors').forEach(el => el.remove());
|
|
284
|
-
return form;
|
|
285
|
+
return wasJQuery ? toJquery(form) : form;
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
static getFormErrorText(errors) {
|
package/media.js
CHANGED
package/package.json
CHANGED
package/select_all.js
CHANGED
|
@@ -11,6 +11,9 @@ class SelectAll {
|
|
|
11
11
|
linkClone.addEventListener('click', function(e) {
|
|
12
12
|
e.preventDefault();
|
|
13
13
|
const formGroup = this.closest('.form-group');
|
|
14
|
+
if (!formGroup) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
14
17
|
const allCheckbox = formGroup.querySelectorAll('input[type="checkbox"]:not(.check_all)');
|
|
15
18
|
const allCheckboxChecked = formGroup.querySelectorAll('input[type="checkbox"]:not(.check_all):checked');
|
|
16
19
|
const allCheckboxWithCheckAll = formGroup.querySelectorAll('input[type="checkbox"]');
|
|
@@ -20,6 +23,9 @@ class SelectAll {
|
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
const formGroup = linkClone.closest('.form-group');
|
|
26
|
+
if (!formGroup) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
23
29
|
formGroup.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
|
24
30
|
cb.addEventListener('change', () => {
|
|
25
31
|
SelectAll.updateFormGroup(cb.closest('.form-group'));
|
|
@@ -30,6 +36,9 @@ class SelectAll {
|
|
|
30
36
|
|
|
31
37
|
static updateFormGroup(formGroup) {
|
|
32
38
|
formGroup = toEl(formGroup);
|
|
39
|
+
if (!formGroup) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
33
42
|
const allCheckbox = formGroup.querySelectorAll('input[type="checkbox"]:not(.check_all)');
|
|
34
43
|
const allCheckboxChecked = formGroup.querySelectorAll('input[type="checkbox"]:not(.check_all):checked');
|
|
35
44
|
const lienSelectAll = formGroup.querySelector('a.check_all');
|
|
@@ -48,6 +57,9 @@ class SelectAll {
|
|
|
48
57
|
|
|
49
58
|
static initInTable(table) {
|
|
50
59
|
table = toEl(table);
|
|
60
|
+
if (!table) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
51
63
|
const inputCheckAll = table.querySelector('tr input.check_all');
|
|
52
64
|
if (!inputCheckAll) {
|
|
53
65
|
return;
|
|
@@ -72,6 +84,9 @@ class SelectAll {
|
|
|
72
84
|
|
|
73
85
|
static updateTable(table) {
|
|
74
86
|
table = toEl(table);
|
|
87
|
+
if (!table) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
75
90
|
const allCheckbox = table.querySelectorAll('tbody input[type="checkbox"]');
|
|
76
91
|
const allCheckboxChecked = table.querySelectorAll('tbody input[type="checkbox"]:checked');
|
|
77
92
|
const checkboxSelectAll = table.querySelector('thead input.check_all');
|
|
@@ -85,6 +100,9 @@ class SelectAll {
|
|
|
85
100
|
|
|
86
101
|
static initDiv(contentDiv) {
|
|
87
102
|
contentDiv = toEl(contentDiv);
|
|
103
|
+
if (!contentDiv) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
88
106
|
contentDiv.querySelectorAll('input.check_all').forEach(inputCheckAll => {
|
|
89
107
|
const div = inputCheckAll.closest('div.checkbox_with_check_all');
|
|
90
108
|
|
|
@@ -92,6 +110,9 @@ class SelectAll {
|
|
|
92
110
|
inputCheckAll.parentElement.replaceChild(clone, inputCheckAll);
|
|
93
111
|
clone.addEventListener('click', function() {
|
|
94
112
|
const d = this.closest('div.checkbox_with_check_all');
|
|
113
|
+
if (!d) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
95
116
|
const allCheckbox = d.querySelectorAll('input[type="checkbox"]:not(.check_all)');
|
|
96
117
|
const allCheckboxChecked = d.querySelectorAll('input[type="checkbox"]:not(.check_all):checked');
|
|
97
118
|
const newState = allCheckbox.length !== allCheckboxChecked.length;
|
|
@@ -99,6 +120,9 @@ class SelectAll {
|
|
|
99
120
|
SelectAll.updateDiv(d);
|
|
100
121
|
});
|
|
101
122
|
|
|
123
|
+
if (!div) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
102
126
|
div.querySelectorAll('div.checkbox input[type="checkbox"], div.form-check input[type="checkbox"]').forEach(cb => {
|
|
103
127
|
cb.addEventListener('change', () => {
|
|
104
128
|
SelectAll.updateDiv(cb.closest('div.checkbox_with_check_all'));
|
|
@@ -110,6 +134,9 @@ class SelectAll {
|
|
|
110
134
|
|
|
111
135
|
static updateDiv(div) {
|
|
112
136
|
div = toEl(div);
|
|
137
|
+
if (!div) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
113
140
|
// 22/11/2021 : rajout :not(.check_all) sinon si toutes les cases sont coché, la case select all n'est pas coché à l'initialisation
|
|
114
141
|
const allCheckbox = div.querySelectorAll('div.checkbox input[type="checkbox"]:not(.check_all), div.form-check input[type="checkbox"]:not(.check_all)');
|
|
115
142
|
const allCheckboxChecked = div.querySelectorAll('div.checkbox input[type="checkbox"]:not(.check_all):checked, div.form-check input[type="checkbox"]:not(.check_all):checked');
|
package/tests/chartjs.test.js
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { Chartjs } = require('../chartjs');
|
|
5
5
|
|
|
6
|
-
// ─── chart creation helpers ──────────────────────────────────────────────────
|
|
7
|
-
|
|
8
6
|
let mockChartInstance;
|
|
9
7
|
|
|
10
8
|
beforeEach(() => {
|
|
@@ -13,11 +11,14 @@ beforeEach(() => {
|
|
|
13
11
|
mockChartInstance.config = config;
|
|
14
12
|
return mockChartInstance;
|
|
15
13
|
});
|
|
14
|
+
global.Chart.register = jest.fn();
|
|
15
|
+
Chartjs.initialized = false;
|
|
16
16
|
HTMLCanvasElement.prototype.getContext = jest.fn(() => ({}));
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
20
20
|
delete global.Chart;
|
|
21
|
+
Chartjs.initialized = false;
|
|
21
22
|
document.body.innerHTML = '';
|
|
22
23
|
jest.restoreAllMocks();
|
|
23
24
|
});
|
|
@@ -28,9 +29,150 @@ function makeCanvas() {
|
|
|
28
29
|
return canvas;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
// ─── groupByPeriod ───────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
32
|
describe('Chartjs', () => {
|
|
33
|
+
|
|
34
|
+
describe('init', () => {
|
|
35
|
+
test('registers centerText plugin on first call', () => {
|
|
36
|
+
Chartjs.init();
|
|
37
|
+
expect(global.Chart.register).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(global.Chart.register).toHaveBeenCalledWith(
|
|
39
|
+
expect.objectContaining({ id: 'centerText' })
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('does not register plugin on subsequent calls', () => {
|
|
44
|
+
Chartjs.init();
|
|
45
|
+
global.Chart.register.mockClear();
|
|
46
|
+
Chartjs.init();
|
|
47
|
+
expect(global.Chart.register).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('sets Chartjs.initialized to true', () => {
|
|
51
|
+
Chartjs.init();
|
|
52
|
+
expect(Chartjs.initialized).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('createStackedChart', () => {
|
|
57
|
+
const chartData = { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] };
|
|
58
|
+
|
|
59
|
+
test('clears div and calls Chart constructor with type bar', () => {
|
|
60
|
+
const canvas = makeCanvas();
|
|
61
|
+
canvas.innerHTML = '<span>old</span>';
|
|
62
|
+
Chartjs.createStackedChart(canvas, chartData);
|
|
63
|
+
expect(canvas.innerHTML).toBe('');
|
|
64
|
+
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('merges user options', () => {
|
|
68
|
+
Chartjs.createStackedChart(makeCanvas(), chartData, null, { options: { responsive: false } });
|
|
69
|
+
expect(mockChartInstance.config.options.responsive).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('title displayed when provided', () => {
|
|
73
|
+
Chartjs.createStackedChart(makeCanvas(), chartData, 'My Title');
|
|
74
|
+
expect(mockChartInstance.config.options.plugins.title.display).toBe(true);
|
|
75
|
+
expect(mockChartInstance.config.options.plugins.title.text).toBe('My Title');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('title not displayed when null', () => {
|
|
79
|
+
Chartjs.createStackedChart(makeCanvas(), chartData, null);
|
|
80
|
+
expect(mockChartInstance.config.options.plugins.title.display).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('tooltip label callback', () => {
|
|
84
|
+
Chartjs.createStackedChart(makeCanvas(), chartData);
|
|
85
|
+
const fn = mockChartInstance.config.options.plugins.tooltip.callbacks.label;
|
|
86
|
+
expect(fn({ dataset: { label: 'Test' }, parsed: { y: 42 } })).toBe('Test: 42');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('accepts jQuery-like object', () => {
|
|
90
|
+
const canvas = makeCanvas();
|
|
91
|
+
const jq = { jquery: '3.6.0', 0: canvas, length: 1 };
|
|
92
|
+
expect(() => Chartjs.createStackedChart(jq, chartData)).not.toThrow();
|
|
93
|
+
expect(global.Chart).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('createBarChart', () => {
|
|
98
|
+
const chartData = { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] };
|
|
99
|
+
|
|
100
|
+
test('clears div and calls Chart constructor with type bar', () => {
|
|
101
|
+
const canvas = makeCanvas();
|
|
102
|
+
canvas.innerHTML = '<span>old</span>';
|
|
103
|
+
Chartjs.createBarChart(canvas, chartData);
|
|
104
|
+
expect(canvas.innerHTML).toBe('');
|
|
105
|
+
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('title displayed when provided', () => {
|
|
109
|
+
Chartjs.createBarChart(makeCanvas(), chartData, 'My Title');
|
|
110
|
+
expect(mockChartInstance.config.options.plugins.title.display).toBe(true);
|
|
111
|
+
expect(mockChartInstance.config.options.plugins.title.text).toBe('My Title');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('title not displayed when null', () => {
|
|
115
|
+
Chartjs.createBarChart(makeCanvas(), chartData, null);
|
|
116
|
+
expect(mockChartInstance.config.options.plugins.title.display).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('tooltip label callback', () => {
|
|
120
|
+
Chartjs.createBarChart(makeCanvas(), chartData);
|
|
121
|
+
const fn = mockChartInstance.config.options.plugins.tooltip.callbacks.label;
|
|
122
|
+
expect(fn({ dataset: { label: 'Test' }, parsed: { y: 10 } })).toBe('Test : 10');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('accepts jQuery-like object', () => {
|
|
126
|
+
const canvas = makeCanvas();
|
|
127
|
+
const jq = { jquery: '3.6.0', 0: canvas, length: 1 };
|
|
128
|
+
expect(() => Chartjs.createBarChart(jq, chartData)).not.toThrow();
|
|
129
|
+
expect(global.Chart).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('createLineChart', () => {
|
|
134
|
+
const chartData = { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] };
|
|
135
|
+
|
|
136
|
+
test('clears div and calls Chart constructor with type line', () => {
|
|
137
|
+
const canvas = makeCanvas();
|
|
138
|
+
Chartjs.createLineChart(canvas, chartData);
|
|
139
|
+
expect(canvas.innerHTML).toBe('');
|
|
140
|
+
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'line' }));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('tooltip label callback', () => {
|
|
144
|
+
Chartjs.createLineChart(makeCanvas(), chartData);
|
|
145
|
+
const fn = mockChartInstance.config.options.plugins.tooltip.callbacks.label;
|
|
146
|
+
expect(fn({ dataset: { label: 'Line' }, parsed: { y: 7 } })).toBe('Line : 7');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('createDoughnutChart', () => {
|
|
151
|
+
test('clears div and calls Chart constructor with type doughnut', () => {
|
|
152
|
+
const canvas = makeCanvas();
|
|
153
|
+
Chartjs.createDoughnutChart(canvas, { labels: ['A'], values: [1], colors: ['#f00'] });
|
|
154
|
+
expect(canvas.innerHTML).toBe('');
|
|
155
|
+
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'doughnut' }));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('uses chartData.values as dataset data', () => {
|
|
159
|
+
Chartjs.createDoughnutChart(makeCanvas(), { labels: ['A', 'B'], values: [30, 70], colors: ['#f00', '#00f'] });
|
|
160
|
+
expect(mockChartInstance.config.data.datasets[0].data).toEqual([30, 70]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('tooltip label shows percentage', () => {
|
|
164
|
+
Chartjs.createDoughnutChart(makeCanvas(), { labels: ['A', 'B'], values: [30, 70], colors: ['#f00', '#00f'] });
|
|
165
|
+
const fn = mockChartInstance.config.options.plugins.tooltip.callbacks.label;
|
|
166
|
+
expect(fn({ label: 'A', raw: 30, dataset: { data: [30, 70] } })).toBe('A: 30 (30.0%)');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('tooltip label for 100%', () => {
|
|
170
|
+
Chartjs.createDoughnutChart(makeCanvas(), { labels: ['Only'], values: [100], colors: ['#f00'] });
|
|
171
|
+
const fn = mockChartInstance.config.options.plugins.tooltip.callbacks.label;
|
|
172
|
+
expect(fn({ label: 'Only', raw: 100, dataset: { data: [100] } })).toBe('Only: 100 (100.0%)');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
34
176
|
describe('groupByPeriod', () => {
|
|
35
177
|
test('should group data by day (default)', () => {
|
|
36
178
|
const data = {
|
|
@@ -38,10 +180,7 @@ describe('Chartjs', () => {
|
|
|
38
180
|
'2024-01-16': { views: 20, clicks: 8 },
|
|
39
181
|
'2024-01-17': { views: 15, clicks: 6 }
|
|
40
182
|
};
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const result = Chartjs.groupByPeriod(data, 'day', metrics);
|
|
44
|
-
|
|
183
|
+
const result = Chartjs.groupByPeriod(data, 'day', ['views', 'clicks']);
|
|
45
184
|
expect(result).toHaveLength(3);
|
|
46
185
|
expect(result[0]).toEqual({ label: '2024-01-15', views: 10, clicks: 5 });
|
|
47
186
|
expect(result[1]).toEqual({ label: '2024-01-16', views: 20, clicks: 8 });
|
|
@@ -54,17 +193,12 @@ describe('Chartjs', () => {
|
|
|
54
193
|
'2024-01-20': { views: 20, clicks: 8 },
|
|
55
194
|
'2024-02-05': { views: 15, clicks: 6 }
|
|
56
195
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const result = Chartjs.groupByPeriod(data, 'month', metrics);
|
|
60
|
-
|
|
196
|
+
const result = Chartjs.groupByPeriod(data, 'month', ['views', 'clicks']);
|
|
61
197
|
expect(result).toHaveLength(2);
|
|
62
198
|
expect(result[0].label).toBe('2024-01');
|
|
63
199
|
expect(result[0].views).toBe(15);
|
|
64
200
|
expect(result[0].clicks).toBe(6.5);
|
|
65
201
|
expect(result[1].label).toBe('2024-02');
|
|
66
|
-
expect(result[1].views).toBe(15);
|
|
67
|
-
expect(result[1].clicks).toBe(6);
|
|
68
202
|
});
|
|
69
203
|
|
|
70
204
|
test('should group data by week', () => {
|
|
@@ -74,10 +208,7 @@ describe('Chartjs', () => {
|
|
|
74
208
|
'2024-01-08': { views: 15 },
|
|
75
209
|
'2024-01-09': { views: 25 }
|
|
76
210
|
};
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const result = Chartjs.groupByPeriod(data, 'week', metrics);
|
|
80
|
-
|
|
211
|
+
const result = Chartjs.groupByPeriod(data, 'week', ['views']);
|
|
81
212
|
expect(result.length).toBeGreaterThan(0);
|
|
82
213
|
result.forEach(item => {
|
|
83
214
|
expect(item.label).toMatch(/^\d{4}-S\d+$/);
|
|
@@ -85,34 +216,20 @@ describe('Chartjs', () => {
|
|
|
85
216
|
});
|
|
86
217
|
|
|
87
218
|
test('should handle single metric', () => {
|
|
88
|
-
const data = {
|
|
89
|
-
|
|
90
|
-
'2024-01-16': { views: 20 }
|
|
91
|
-
};
|
|
92
|
-
const metrics = ['views'];
|
|
93
|
-
|
|
94
|
-
const result = Chartjs.groupByPeriod(data, 'day', metrics);
|
|
95
|
-
|
|
219
|
+
const data = { '2024-01-15': { views: 10 }, '2024-01-16': { views: 20 } };
|
|
220
|
+
const result = Chartjs.groupByPeriod(data, 'day', ['views']);
|
|
96
221
|
expect(result).toHaveLength(2);
|
|
97
222
|
expect(result[0]).toEqual({ label: '2024-01-15', views: 10 });
|
|
98
|
-
expect(result[1]).toEqual({ label: '2024-01-16', views: 20 });
|
|
99
223
|
});
|
|
100
224
|
|
|
101
225
|
test('should handle multiple metrics', () => {
|
|
102
|
-
const data = {
|
|
103
|
-
|
|
104
|
-
};
|
|
105
|
-
const metrics = ['views', 'clicks', 'conversions'];
|
|
106
|
-
|
|
107
|
-
const result = Chartjs.groupByPeriod(data, 'day', metrics);
|
|
108
|
-
|
|
109
|
-
expect(result).toHaveLength(1);
|
|
226
|
+
const data = { '2024-01-15': { views: 10, clicks: 5, conversions: 2 } };
|
|
227
|
+
const result = Chartjs.groupByPeriod(data, 'day', ['views', 'clicks', 'conversions']);
|
|
110
228
|
expect(result[0]).toEqual({ label: '2024-01-15', views: 10, clicks: 5, conversions: 2 });
|
|
111
229
|
});
|
|
112
230
|
|
|
113
231
|
test('should handle empty data', () => {
|
|
114
|
-
|
|
115
|
-
expect(result).toEqual([]);
|
|
232
|
+
expect(Chartjs.groupByPeriod({}, 'day', ['views'])).toEqual([]);
|
|
116
233
|
});
|
|
117
234
|
|
|
118
235
|
test('should average values when grouping by month', () => {
|
|
@@ -122,37 +239,42 @@ describe('Chartjs', () => {
|
|
|
122
239
|
'2024-01-20': { score: 300 }
|
|
123
240
|
};
|
|
124
241
|
const result = Chartjs.groupByPeriod(data, 'month', ['score']);
|
|
125
|
-
|
|
126
|
-
expect(result).toHaveLength(1);
|
|
127
|
-
expect(result[0].label).toBe('2024-01');
|
|
128
242
|
expect(result[0].score).toBe(200);
|
|
129
243
|
});
|
|
130
244
|
|
|
131
245
|
test('should handle missing metric values', () => {
|
|
132
|
-
const data = {
|
|
133
|
-
'2024-01-15': { views: 10 },
|
|
134
|
-
'2024-01-16': { views: 20, clicks: 5 }
|
|
135
|
-
};
|
|
246
|
+
const data = { '2024-01-15': { views: 10 }, '2024-01-16': { views: 20, clicks: 5 } };
|
|
136
247
|
const result = Chartjs.groupByPeriod(data, 'day', ['views', 'clicks']);
|
|
137
|
-
|
|
138
|
-
expect(result).toHaveLength(2);
|
|
139
248
|
expect(result[0]).toEqual({ label: '2024-01-15', views: 10, clicks: NaN });
|
|
140
249
|
expect(result[1]).toEqual({ label: '2024-01-16', views: 20, clicks: 5 });
|
|
141
250
|
});
|
|
142
251
|
|
|
143
252
|
test('should handle dates across different years', () => {
|
|
144
|
-
const data = {
|
|
145
|
-
'2023-12-30': { count: 5 },
|
|
146
|
-
'2024-01-02': { count: 10 }
|
|
147
|
-
};
|
|
253
|
+
const data = { '2023-12-30': { count: 5 }, '2024-01-02': { count: 10 } };
|
|
148
254
|
const result = Chartjs.groupByPeriod(data, 'month', ['count']);
|
|
149
|
-
|
|
150
|
-
expect(result).toHaveLength(2);
|
|
151
255
|
expect(result[0].label).toBe('2023-12');
|
|
152
256
|
expect(result[1].label).toBe('2024-01');
|
|
153
257
|
});
|
|
154
258
|
});
|
|
155
259
|
|
|
260
|
+
describe('getPeriodLabels', () => {
|
|
261
|
+
const { DatePeriod } = require('../date_time');
|
|
262
|
+
|
|
263
|
+
test('delegates to DatePeriod.getPeriodLabels with default locale/timezone', () => {
|
|
264
|
+
const spy = jest.spyOn(DatePeriod, 'getPeriodLabels');
|
|
265
|
+
const data = { '2024-01-01': {}, '2024-02-01': {} };
|
|
266
|
+
Chartjs.getPeriodLabels(data, 'month');
|
|
267
|
+
expect(spy).toHaveBeenCalledWith(Object.keys(data), 'month', 'fr-FR', 'Europe/Paris');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('passes custom locale and timezone', () => {
|
|
271
|
+
const spy = jest.spyOn(DatePeriod, 'getPeriodLabels');
|
|
272
|
+
const data = { '2024-01-01': {} };
|
|
273
|
+
Chartjs.getPeriodLabels(data, 'month', 'en-US', 'America/New_York');
|
|
274
|
+
expect(spy).toHaveBeenCalledWith(Object.keys(data), 'month', 'en-US', 'America/New_York');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
156
278
|
describe('getAutoGranularity', () => {
|
|
157
279
|
test('should return day_of_month for data spanning 30 days or less', () => {
|
|
158
280
|
expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-01-15': {}, '2024-01-30': {} })).toBe('day_of_month');
|
|
@@ -191,99 +313,4 @@ describe('Chartjs', () => {
|
|
|
191
313
|
});
|
|
192
314
|
});
|
|
193
315
|
|
|
194
|
-
describe('chart creation', () => {
|
|
195
|
-
const chartData = { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] };
|
|
196
|
-
|
|
197
|
-
test('createStackedChart clears div and calls Chart constructor', () => {
|
|
198
|
-
const canvas = makeCanvas();
|
|
199
|
-
canvas.innerHTML = '<span>old</span>';
|
|
200
|
-
|
|
201
|
-
Chartjs.createStackedChart(canvas, chartData);
|
|
202
|
-
|
|
203
|
-
expect(canvas.innerHTML).toBe('');
|
|
204
|
-
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('createStackedChart merges user options', () => {
|
|
208
|
-
Chartjs.createStackedChart(makeCanvas(), chartData, null, { options: { responsive: false } });
|
|
209
|
-
|
|
210
|
-
expect(mockChartInstance.config.options.responsive).toBe(false);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('createBarChart clears div and calls Chart constructor', () => {
|
|
214
|
-
const canvas = makeCanvas();
|
|
215
|
-
canvas.innerHTML = '<span>old</span>';
|
|
216
|
-
|
|
217
|
-
Chartjs.createBarChart(canvas, chartData);
|
|
218
|
-
|
|
219
|
-
expect(canvas.innerHTML).toBe('');
|
|
220
|
-
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('createBarChart sets title when provided', () => {
|
|
224
|
-
Chartjs.createBarChart(makeCanvas(), chartData, 'My Title');
|
|
225
|
-
|
|
226
|
-
expect(mockChartInstance.config.options.plugins.title.display).toBe(true);
|
|
227
|
-
expect(mockChartInstance.config.options.plugins.title.text).toBe('My Title');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('createLineChart clears div and calls Chart constructor', () => {
|
|
231
|
-
const canvas = makeCanvas();
|
|
232
|
-
Chartjs.createLineChart(canvas, chartData);
|
|
233
|
-
|
|
234
|
-
expect(canvas.innerHTML).toBe('');
|
|
235
|
-
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'line' }));
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('createDoughnutChart clears div and calls Chart constructor', () => {
|
|
239
|
-
const canvas = makeCanvas();
|
|
240
|
-
Chartjs.createDoughnutChart(canvas, { labels: ['A'], values: [1], colors: ['#f00'] });
|
|
241
|
-
|
|
242
|
-
expect(canvas.innerHTML).toBe('');
|
|
243
|
-
expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'doughnut' }));
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('createDoughnutChart uses chartData.values as dataset data', () => {
|
|
247
|
-
Chartjs.createDoughnutChart(makeCanvas(), { labels: ['A', 'B'], values: [30, 70], colors: ['#f00', '#00f'] });
|
|
248
|
-
|
|
249
|
-
expect(mockChartInstance.config.data.datasets[0].data).toEqual([30, 70]);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('title not displayed when null', () => {
|
|
253
|
-
Chartjs.createBarChart(makeCanvas(), chartData, null);
|
|
254
|
-
|
|
255
|
-
expect(mockChartInstance.config.options.plugins.title.display).toBe(false);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('jQuery compatibility (toEl)', () => {
|
|
259
|
-
test('createStackedChart accepts jQuery-like object', () => {
|
|
260
|
-
const canvas = makeCanvas();
|
|
261
|
-
const jq = { jquery: '3.6.0', 0: canvas, length: 1 };
|
|
262
|
-
|
|
263
|
-
expect(() => Chartjs.createStackedChart(jq, chartData)).not.toThrow();
|
|
264
|
-
expect(global.Chart).toHaveBeenCalled();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test('createBarChart accepts jQuery-like object', () => {
|
|
268
|
-
const canvas = makeCanvas();
|
|
269
|
-
const jq = { jquery: '3.6.0', 0: canvas, length: 1 };
|
|
270
|
-
|
|
271
|
-
expect(() => Chartjs.createBarChart(jq, chartData)).not.toThrow();
|
|
272
|
-
expect(global.Chart).toHaveBeenCalled();
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe('class structure', () => {
|
|
278
|
-
test('should have all expected static methods', () => {
|
|
279
|
-
expect(typeof Chartjs.init).toBe('function');
|
|
280
|
-
expect(typeof Chartjs.createStackedChart).toBe('function');
|
|
281
|
-
expect(typeof Chartjs.createBarChart).toBe('function');
|
|
282
|
-
expect(typeof Chartjs.createLineChart).toBe('function');
|
|
283
|
-
expect(typeof Chartjs.createDoughnutChart).toBe('function');
|
|
284
|
-
expect(typeof Chartjs.groupByPeriod).toBe('function');
|
|
285
|
-
expect(typeof Chartjs.getPeriodLabels).toBe('function');
|
|
286
|
-
expect(typeof Chartjs.getAutoGranularity).toBe('function');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
316
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
require('../array'); // For removeEmptyValues method
|
|
5
|
+
require('../string'); // For normalizeBreaks method
|
|
5
6
|
const { FormHelper, ArrayField, EditValue } = require('../form_helper');
|
|
6
7
|
|
|
7
8
|
// Helper functions
|
|
@@ -43,9 +44,15 @@ function addSelect(form, name, options = []) {
|
|
|
43
44
|
return select;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// Simule un objet jQuery wrappant un élément DOM natif
|
|
48
|
+
function mockJQuery(el) {
|
|
49
|
+
return { jquery: '3.0', 0: el, length: 1 };
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
afterEach(() => {
|
|
47
53
|
document.body.innerHTML = '';
|
|
48
54
|
jest.clearAllMocks();
|
|
55
|
+
delete global.$;
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
describe('FormHelper', () => {
|
|
@@ -88,6 +95,18 @@ describe('FormHelper', () => {
|
|
|
88
95
|
expect(onSubmitCallback).toHaveBeenCalledWith(form, customBtn);
|
|
89
96
|
});
|
|
90
97
|
|
|
98
|
+
test('should return jQuery wrapper when passed a jQuery object', () => {
|
|
99
|
+
const form = setupForm();
|
|
100
|
+
addButton(form, 'validate', 'Submit');
|
|
101
|
+
const jqForm = mockJQuery(form);
|
|
102
|
+
global.$ = jest.fn(el => mockJQuery(el));
|
|
103
|
+
|
|
104
|
+
const result = FormHelper.init(jqForm, jest.fn());
|
|
105
|
+
|
|
106
|
+
expect(result).toHaveProperty('jquery');
|
|
107
|
+
expect(result[0]).toBe(form);
|
|
108
|
+
});
|
|
109
|
+
|
|
91
110
|
test('should call buttonLoader with loading on submit', () => {
|
|
92
111
|
const form = setupForm();
|
|
93
112
|
const btn = addButton(form, 'validate', 'Submit');
|
|
@@ -521,6 +540,17 @@ describe('FormHelper', () => {
|
|
|
521
540
|
expect(form.querySelectorAll('div.form_errors').length).toBe(0);
|
|
522
541
|
expect(result).toBe(form);
|
|
523
542
|
});
|
|
543
|
+
|
|
544
|
+
test('should return jQuery wrapper when passed a jQuery object', () => {
|
|
545
|
+
const form = setupForm();
|
|
546
|
+
const jqForm = mockJQuery(form);
|
|
547
|
+
global.$ = jest.fn(el => mockJQuery(el));
|
|
548
|
+
|
|
549
|
+
const result = FormHelper.hideFormErrors(jqForm);
|
|
550
|
+
|
|
551
|
+
expect(result).toHaveProperty('jquery');
|
|
552
|
+
expect(result[0]).toBe(form);
|
|
553
|
+
});
|
|
524
554
|
});
|
|
525
555
|
|
|
526
556
|
describe('displayFormErrorsFromText', () => {
|
|
@@ -909,23 +939,306 @@ describe('FormHelper', () => {
|
|
|
909
939
|
});
|
|
910
940
|
|
|
911
941
|
describe('ArrayField', () => {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
942
|
+
let container;
|
|
943
|
+
|
|
944
|
+
beforeEach(() => {
|
|
945
|
+
container = document.createElement('div');
|
|
946
|
+
document.body.appendChild(container);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
afterEach(() => {
|
|
950
|
+
document.body.innerHTML = '';
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
function makeOptions(overrides = {}) {
|
|
954
|
+
return Object.assign({
|
|
955
|
+
entering_field_in_table: false,
|
|
956
|
+
add_one_button_enabled: true,
|
|
957
|
+
add_multi_button_enabled: false,
|
|
958
|
+
input_name: 'items[]',
|
|
959
|
+
item_name: 'Item',
|
|
960
|
+
}, overrides);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// jsdom applies HTML5 foster-parenting: <tr> inside <div>.innerHTML gets stripped.
|
|
964
|
+
// ArrayField.init falls back to cloneNode when a .base template row exists,
|
|
965
|
+
// so we pre-populate the container with a base row to make row tests work.
|
|
966
|
+
function addBaseRow(opts = {}) {
|
|
967
|
+
const enteringInTable = opts.entering_field_in_table ?? false;
|
|
968
|
+
const inputName = opts.input_name ?? 'items[]';
|
|
969
|
+
let tdContent, links;
|
|
970
|
+
if (enteringInTable) {
|
|
971
|
+
tdContent = `<input type="text" name="${inputName}" class="form-control">`;
|
|
972
|
+
links = '<a href="#" class="add btn btn-sm btn-success ms-1"></a><a href="#" class="remove btn btn-sm btn-danger ms-1"></a>';
|
|
973
|
+
} else {
|
|
974
|
+
tdContent = `<input type="hidden" name="${inputName}"> <span class="value"></span>`;
|
|
975
|
+
links = '<a href="#" class="remove btn btn-sm btn-danger ms-1"></a>';
|
|
976
|
+
}
|
|
977
|
+
container.innerHTML = `<table class="table table-sm"><tbody><tr class="base hide"><td class="table-input">${tdContent}</td><td class="table-links">${links}</td></tr></tbody></table>`;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
test('creates table and list_empty if not present', () => {
|
|
981
|
+
ArrayField.init(container, [], makeOptions());
|
|
982
|
+
expect(container.querySelector('table')).not.toBeNull();
|
|
983
|
+
expect(container.querySelector('.list_empty')).not.toBeNull();
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test('shows list_empty and hides table when no items', () => {
|
|
987
|
+
ArrayField.init(container, [], makeOptions());
|
|
988
|
+
expect(container.querySelector('.list_empty').classList.contains('hide')).toBe(false);
|
|
989
|
+
expect(container.querySelector('table').classList.contains('hide')).toBe(true);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test('populates default values as rows', () => {
|
|
993
|
+
addBaseRow();
|
|
994
|
+
ArrayField.init(container, ['foo', 'bar'], makeOptions());
|
|
995
|
+
const rows = container.querySelectorAll('table tbody tr:not(.base)');
|
|
996
|
+
expect(rows).toHaveLength(2);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('table is visible when items are present', () => {
|
|
1000
|
+
addBaseRow();
|
|
1001
|
+
ArrayField.init(container, ['item1'], makeOptions());
|
|
1002
|
+
expect(container.querySelector('table').classList.contains('hide')).toBe(false);
|
|
1003
|
+
expect(container.querySelector('.list_empty').classList.contains('hide')).toBe(true);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test('row contains hidden input and span.value for entering_field_in_table=false', () => {
|
|
1007
|
+
addBaseRow({ entering_field_in_table: false });
|
|
1008
|
+
ArrayField.init(container, ['hello'], makeOptions({ entering_field_in_table: false }));
|
|
1009
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
1010
|
+
expect(row.querySelector('input[type="hidden"]')).not.toBeNull();
|
|
1011
|
+
expect(row.querySelector('span.value')).not.toBeNull();
|
|
1012
|
+
expect(row.querySelector('input[type="hidden"]').value).toBe('hello');
|
|
1013
|
+
expect(row.querySelector('span.value').textContent).toBe('hello');
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test('row contains text input for entering_field_in_table=true', () => {
|
|
1017
|
+
addBaseRow({ entering_field_in_table: true });
|
|
1018
|
+
ArrayField.init(container, ['hello'], makeOptions({ entering_field_in_table: true, nb_min_lines: 0 }));
|
|
1019
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
1020
|
+
expect(row.querySelector('input[type="text"]')).not.toBeNull();
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
test('creates add_one button when add_one_button_enabled', () => {
|
|
1024
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1025
|
+
expect(container.querySelector('a.add_one')).not.toBeNull();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test('does not create add_one button when disabled', () => {
|
|
1029
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: false }));
|
|
1030
|
+
expect(container.querySelector('a.add_one')).toBeNull();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test('clicking add_one shows item_add_one div', () => {
|
|
1034
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1035
|
+
container.querySelector('a.add_one').click();
|
|
1036
|
+
expect(container.querySelector('.item_add_one').classList.contains('hide')).toBe(false);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
test('clicking cancel in add_one hides item_add_one div', () => {
|
|
1040
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1041
|
+
container.querySelector('a.add_one').click();
|
|
1042
|
+
container.querySelector('.item_add_one a.cancel').click();
|
|
1043
|
+
expect(container.querySelector('.item_add_one').classList.contains('hide')).toBe(true);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
test('clicking add in item_add_one adds a new row', () => {
|
|
1047
|
+
addBaseRow();
|
|
1048
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1049
|
+
container.querySelector('a.add_one').click();
|
|
1050
|
+
container.querySelector('.item_add_one input.form-control').value = 'newitem';
|
|
1051
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1052
|
+
const rows = container.querySelectorAll('table tbody tr:not(.base)');
|
|
1053
|
+
expect(rows).toHaveLength(1);
|
|
1054
|
+
expect(rows[0].querySelector('span.value').textContent).toBe('newitem');
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
test('remove button removes a row', () => {
|
|
1058
|
+
addBaseRow();
|
|
1059
|
+
ArrayField.init(container, ['a', 'b'], makeOptions());
|
|
1060
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(2);
|
|
1061
|
+
container.querySelector('table tbody tr:not(.base) a.remove').click();
|
|
1062
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(1);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
test('calls update_list_callback on changes', () => {
|
|
1066
|
+
addBaseRow();
|
|
1067
|
+
const cb = jest.fn();
|
|
1068
|
+
ArrayField.init(container, ['x'], makeOptions({ update_list_callback: cb }));
|
|
1069
|
+
expect(cb).toHaveBeenCalled();
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test('calls init_callback with container after init', () => {
|
|
1073
|
+
const cb = jest.fn();
|
|
1074
|
+
ArrayField.init(container, [], makeOptions({ init_callback: cb }));
|
|
1075
|
+
expect(cb).toHaveBeenCalledWith(container, expect.any(Function), expect.any(Function));
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test('uses custom list_empty_text', () => {
|
|
1079
|
+
ArrayField.init(container, [], makeOptions({ list_empty_text: 'Nothing here' }));
|
|
1080
|
+
expect(container.querySelector('.list_empty').textContent).toBe('Nothing here');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test('calls get_errors_callback and shows errors on invalid input', () => {
|
|
1084
|
+
addBaseRow();
|
|
1085
|
+
ArrayField.init(container, [], makeOptions({
|
|
1086
|
+
add_one_button_enabled: true,
|
|
1087
|
+
get_errors_callback: () => ['Invalid value'],
|
|
1088
|
+
}));
|
|
1089
|
+
container.querySelector('a.add_one').click();
|
|
1090
|
+
container.querySelector('.item_add_one input.form-control').value = 'bad';
|
|
1091
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1092
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(0);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test('does not add duplicate items', () => {
|
|
1096
|
+
addBaseRow();
|
|
1097
|
+
ArrayField.init(container, ['dup'], makeOptions({ add_one_button_enabled: true }));
|
|
1098
|
+
container.querySelector('a.add_one').click();
|
|
1099
|
+
container.querySelector('.item_add_one input.form-control').value = 'dup';
|
|
1100
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1101
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(1);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test('applies nb_max_lines limit: disables add button when reached', () => {
|
|
1105
|
+
addBaseRow({ entering_field_in_table: true });
|
|
1106
|
+
ArrayField.init(container, ['a', 'b'], makeOptions({
|
|
1107
|
+
entering_field_in_table: true,
|
|
1108
|
+
nb_max_lines: 2,
|
|
1109
|
+
nb_min_lines: 0,
|
|
1110
|
+
}));
|
|
1111
|
+
const addLinks = container.querySelectorAll('table tbody tr:not(.base) a.add');
|
|
1112
|
+
addLinks.forEach(a => expect(a.classList.contains('disabled')).toBe(true));
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test('creates add_multi button when add_multi_button_enabled', () => {
|
|
1116
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1117
|
+
expect(container.querySelector('a.add_multi')).not.toBeNull();
|
|
915
1118
|
});
|
|
916
1119
|
|
|
917
|
-
test('
|
|
918
|
-
|
|
1120
|
+
test('clicking add_multi shows item_add_multi div', () => {
|
|
1121
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1122
|
+
container.querySelector('a.add_multi').click();
|
|
1123
|
+
expect(container.querySelector('.item_add_multi').classList.contains('hide')).toBe(false);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test('clicking cancel in add_multi hides item_add_multi', () => {
|
|
1127
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1128
|
+
container.querySelector('a.add_multi').click();
|
|
1129
|
+
container.querySelector('.item_add_multi a.cancel').click();
|
|
1130
|
+
expect(container.querySelector('.item_add_multi').classList.contains('hide')).toBe(true);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test('add_multi adds multiple items from textarea', () => {
|
|
1134
|
+
addBaseRow();
|
|
1135
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1136
|
+
container.querySelector('a.add_multi').click();
|
|
1137
|
+
container.querySelector('.item_add_multi textarea').value = 'alpha\nbeta\ngamma';
|
|
1138
|
+
container.querySelector('.item_add_multi a.add').click();
|
|
1139
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(3);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test('applies format_entered_value_callback on add', () => {
|
|
1143
|
+
addBaseRow();
|
|
1144
|
+
ArrayField.init(container, [], makeOptions({
|
|
1145
|
+
add_one_button_enabled: true,
|
|
1146
|
+
format_entered_value_callback: v => v.toUpperCase(),
|
|
1147
|
+
}));
|
|
1148
|
+
container.querySelector('a.add_one').click();
|
|
1149
|
+
container.querySelector('.item_add_one input.form-control').value = 'hello';
|
|
1150
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1151
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
1152
|
+
expect(row.querySelector('span.value').textContent).toBe('HELLO');
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
test('entering_field_in_table: remove disabled when only 1 row remains', () => {
|
|
1156
|
+
addBaseRow({ entering_field_in_table: true });
|
|
1157
|
+
ArrayField.init(container, ['only'], makeOptions({ entering_field_in_table: true, nb_min_lines: 0 }));
|
|
1158
|
+
const removeLink = container.querySelector('table tbody tr:not(.base) a.remove');
|
|
1159
|
+
expect(removeLink.classList.contains('disabled')).toBe(true);
|
|
919
1160
|
});
|
|
920
1161
|
});
|
|
921
1162
|
|
|
922
1163
|
describe('EditValue', () => {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1164
|
+
let valueDiv, parent;
|
|
1165
|
+
|
|
1166
|
+
beforeEach(() => {
|
|
1167
|
+
parent = document.createElement('div');
|
|
1168
|
+
valueDiv = document.createElement('span');
|
|
1169
|
+
valueDiv.textContent = 'original';
|
|
1170
|
+
parent.appendChild(valueDiv);
|
|
1171
|
+
document.body.appendChild(parent);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
afterEach(() => {
|
|
1175
|
+
document.body.innerHTML = '';
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
test('appends a pencil link next to valueDiv', () => {
|
|
1179
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1180
|
+
expect(parent.querySelector('a')).not.toBeNull();
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test('clicking pencil link hides spans and links and shows a form', () => {
|
|
1184
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1185
|
+
parent.querySelector('a').click();
|
|
1186
|
+
expect(parent.querySelector('form')).not.toBeNull();
|
|
1187
|
+
expect(valueDiv.classList.contains('hide')).toBe(true);
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test('form contains input pre-filled with current text', () => {
|
|
1191
|
+
valueDiv.textContent = 'current value';
|
|
1192
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1193
|
+
parent.querySelector('a').click();
|
|
1194
|
+
const input = parent.querySelector('form input');
|
|
1195
|
+
expect(input.value).toBe('current value');
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test('form uses data-value attribute when present', () => {
|
|
1199
|
+
valueDiv.dataset.value = 'raw-value';
|
|
1200
|
+
valueDiv.textContent = 'Formatted value';
|
|
1201
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1202
|
+
parent.querySelector('a').click();
|
|
1203
|
+
expect(parent.querySelector('form input').value).toBe('raw-value');
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test('calls onSubmitCallback with new value when submit button clicked', () => {
|
|
1207
|
+
const cb = jest.fn();
|
|
1208
|
+
EditValue.init(valueDiv, cb);
|
|
1209
|
+
parent.querySelector('a').click();
|
|
1210
|
+
parent.querySelector('form input').value = 'new val';
|
|
1211
|
+
parent.querySelector('form button').click();
|
|
1212
|
+
expect(cb).toHaveBeenCalledWith('new val', parent, expect.any(Function));
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
test('on success callback updates span value', () => {
|
|
1216
|
+
let capturedCallback;
|
|
1217
|
+
const cb = jest.fn((newVal, par, done) => { capturedCallback = done; });
|
|
1218
|
+
EditValue.init(valueDiv, cb);
|
|
1219
|
+
parent.querySelector('a').click();
|
|
1220
|
+
parent.querySelector('form input').value = 'updated';
|
|
1221
|
+
parent.querySelector('form button').click();
|
|
1222
|
+
capturedCallback(true);
|
|
1223
|
+
expect(valueDiv.textContent).toBe('updated');
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test('on failure callback does not update span value', () => {
|
|
1227
|
+
let capturedCallback;
|
|
1228
|
+
const cb = jest.fn((newVal, par, done) => { capturedCallback = done; });
|
|
1229
|
+
EditValue.init(valueDiv, cb);
|
|
1230
|
+
parent.querySelector('a').click();
|
|
1231
|
+
parent.querySelector('form input').value = 'updated';
|
|
1232
|
+
parent.querySelector('form button').click();
|
|
1233
|
+
capturedCallback(false);
|
|
1234
|
+
expect(valueDiv.textContent).toBe('original');
|
|
926
1235
|
});
|
|
927
1236
|
|
|
928
|
-
test('
|
|
929
|
-
|
|
1237
|
+
test('uses getInputCallback for custom input element', () => {
|
|
1238
|
+
const getInput = jest.fn(() => '<select><option value="x">X</option></select>');
|
|
1239
|
+
EditValue.init(valueDiv, jest.fn(), getInput);
|
|
1240
|
+
parent.querySelector('a').click();
|
|
1241
|
+
expect(parent.querySelector('form select')).not.toBeNull();
|
|
1242
|
+
expect(getInput).toHaveBeenCalled();
|
|
930
1243
|
});
|
|
931
1244
|
});
|
package/tests/select_all.test.js
CHANGED
|
@@ -82,6 +82,10 @@ describe('SelectAll', () => {
|
|
|
82
82
|
const formGroup = document.querySelector('.form-group');
|
|
83
83
|
expect(() => SelectAll.updateFormGroup(formGroup)).not.toThrow();
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
test('should do nothing when formGroup is null', () => {
|
|
87
|
+
expect(() => SelectAll.updateFormGroup(null)).not.toThrow();
|
|
88
|
+
});
|
|
85
89
|
});
|
|
86
90
|
|
|
87
91
|
describe('initLinkInFormGroup', () => {
|
|
@@ -130,6 +134,20 @@ describe('SelectAll', () => {
|
|
|
130
134
|
});
|
|
131
135
|
expect(formGroup.querySelector('a.check_all').textContent).toBe('Tout désélectionner');
|
|
132
136
|
});
|
|
137
|
+
|
|
138
|
+
test('should not throw when checkbox has no .form-group ancestor', () => {
|
|
139
|
+
// Simule une checkbox hors de tout .form-group (le cas du bug en prod)
|
|
140
|
+
document.body.innerHTML = `
|
|
141
|
+
<div>
|
|
142
|
+
<a class="check_all" href="#">Tout sélectionner</a>
|
|
143
|
+
<input type="checkbox" id="orphan">
|
|
144
|
+
</div>`;
|
|
145
|
+
const link = document.querySelector('a.check_all');
|
|
146
|
+
SelectAll.initLinkInFormGroup(link);
|
|
147
|
+
const cb = document.getElementById('orphan');
|
|
148
|
+
cb.checked = true;
|
|
149
|
+
expect(() => cb.dispatchEvent(new Event('change'))).not.toThrow();
|
|
150
|
+
});
|
|
133
151
|
});
|
|
134
152
|
|
|
135
153
|
describe('updateTable', () => {
|
|
@@ -159,6 +177,10 @@ describe('SelectAll', () => {
|
|
|
159
177
|
</table>`;
|
|
160
178
|
expect(() => SelectAll.updateTable(document.querySelector('table'))).not.toThrow();
|
|
161
179
|
});
|
|
180
|
+
|
|
181
|
+
test('should do nothing when table is null', () => {
|
|
182
|
+
expect(() => SelectAll.updateTable(null)).not.toThrow();
|
|
183
|
+
});
|
|
162
184
|
});
|
|
163
185
|
|
|
164
186
|
describe('initInTable', () => {
|
|
@@ -197,6 +219,10 @@ describe('SelectAll', () => {
|
|
|
197
219
|
});
|
|
198
220
|
expect(table.querySelector('thead input.check_all').checked).toBe(true);
|
|
199
221
|
});
|
|
222
|
+
|
|
223
|
+
test('should do nothing when table is null', () => {
|
|
224
|
+
expect(() => SelectAll.initInTable(null)).not.toThrow();
|
|
225
|
+
});
|
|
200
226
|
});
|
|
201
227
|
|
|
202
228
|
describe('updateDiv', () => {
|
|
@@ -222,6 +248,10 @@ describe('SelectAll', () => {
|
|
|
222
248
|
document.body.innerHTML = `<div class="checkbox_with_check_all"><div class="form-check"><input type="checkbox"></div></div>`;
|
|
223
249
|
expect(() => SelectAll.updateDiv(document.querySelector('.checkbox_with_check_all'))).not.toThrow();
|
|
224
250
|
});
|
|
251
|
+
|
|
252
|
+
test('should do nothing when div is null', () => {
|
|
253
|
+
expect(() => SelectAll.updateDiv(null)).not.toThrow();
|
|
254
|
+
});
|
|
225
255
|
});
|
|
226
256
|
|
|
227
257
|
describe('initDiv', () => {
|
|
@@ -270,5 +300,23 @@ describe('SelectAll', () => {
|
|
|
270
300
|
SelectAll.initDiv(contentDiv);
|
|
271
301
|
expect(contentDiv.querySelector('input.check_all').checked).toBe(true);
|
|
272
302
|
});
|
|
303
|
+
|
|
304
|
+
test('should do nothing when contentDiv is null', () => {
|
|
305
|
+
expect(() => SelectAll.initDiv(null)).not.toThrow();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('should not throw when checkbox has no .checkbox_with_check_all ancestor', () => {
|
|
309
|
+
// check_all orpheline — closest('div.checkbox_with_check_all') retourne null
|
|
310
|
+
document.body.innerHTML = `
|
|
311
|
+
<div id="content">
|
|
312
|
+
<input type="checkbox" class="check_all">
|
|
313
|
+
<div class="form-check"><input type="checkbox" id="orphan"></div>
|
|
314
|
+
</div>`;
|
|
315
|
+
const contentDiv = document.getElementById('content');
|
|
316
|
+
SelectAll.initDiv(contentDiv);
|
|
317
|
+
const cb = document.getElementById('orphan');
|
|
318
|
+
cb.checked = true;
|
|
319
|
+
expect(() => cb.dispatchEvent(new Event('change'))).not.toThrow();
|
|
320
|
+
});
|
|
273
321
|
});
|
|
274
322
|
});
|