@osimatic/helpers-js 1.5.5 → 1.5.6

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/media.js CHANGED
@@ -1,3 +1,6 @@
1
+ const { FormHelper } = require('./form_helper');
2
+ const { HTTPClient } = require('./http_client');
3
+
1
4
  class AudioMedia {
2
5
 
3
6
  static getPlayer(playUrl) {
@@ -6,37 +9,42 @@ class AudioMedia {
6
9
 
7
10
  static initPlayLinks(div) {
8
11
  // Affiche un lecteur audio
9
- div.find('.play_link').off('click').click(function () {
10
- let audio = $(AudioMedia.getPlayer($(this).data('play_url')));
11
- audio[0].play();
12
- $(this).after(audio);
13
- $(this).remove();
14
- return false;
12
+ div.querySelectorAll('.play_link').forEach(link => {
13
+ link.addEventListener('click', function(e) {
14
+ e.preventDefault();
15
+ const template = document.createElement('template');
16
+ template.innerHTML = AudioMedia.getPlayer(this.dataset.play_url);
17
+ const audio = template.content.firstChild;
18
+ audio.play();
19
+ this.insertAdjacentElement('afterend', audio);
20
+ this.remove();
21
+ });
15
22
  });
16
23
 
17
- div.find('.play_asynchronously_link').off('click').click(function () {
18
- //if (FormHelper.buttonLoader($(this), 'loading') != null) {
19
- let button = FormHelper.buttonLoader($(this), 'loading');
20
- AudioMedia.playAudioUrl($(this).data('url'), () => FormHelper.buttonLoader(button, 'reset'));
21
- //} else {
22
- // let button = $(this).attr('disabled', true).button('loading');
23
- // AudioMedia.playAudioUrl($(this).data('url'), () => button.attr('disabled', false).button('reset'));
24
- //}
25
- return false;
24
+ div.querySelectorAll('.play_asynchronously_link').forEach(link => {
25
+ link.addEventListener('click', function(e) {
26
+ e.preventDefault();
27
+ let button = FormHelper.buttonLoader(this, 'loading');
28
+ AudioMedia.playAudioUrl(this.dataset.url, () => FormHelper.buttonLoader(button, 'reset'));
29
+ });
26
30
  });
27
31
 
28
- div.find('.modal_play_link').off('click').click(function () {
29
- $('#modal_voice_message_play').on('show.bs.modal', function (event) {
30
- let button = $(event.relatedTarget);
31
- let modal = $(this);
32
-
33
- let player = modal.find('audio');
34
- player.prop('src', button.data('play_url'));
35
- player.play();
32
+ div.querySelectorAll('.modal_play_link').forEach(link => {
33
+ link.addEventListener('click', function(e) {
34
+ e.preventDefault();
35
+ const modalEl = document.getElementById('modal_voice_message_play');
36
+ if (!modalEl) return;
37
+ const currentLink = this;
38
+ modalEl.addEventListener('show.bs.modal', function handler(event) {
39
+ modalEl.removeEventListener('show.bs.modal', handler);
40
+ const player = modalEl.querySelector('audio');
41
+ if (player) {
42
+ player.src = (event.relatedTarget || currentLink).dataset.play_url;
43
+ player.play();
44
+ }
45
+ });
46
+ bootstrap.Modal.getOrCreateInstance(modalEl).show(this);
36
47
  });
37
-
38
- $('#modal_voice_message_play').modal('show', $(this));
39
- return false;
40
48
  });
41
49
  }
42
50
 
@@ -117,12 +125,12 @@ class AudioMedia {
117
125
 
118
126
  class VideoMedia {
119
127
  static initPlayPauseClick(videoElement) {
120
- $(videoElement).click(function(e) {
128
+ videoElement.addEventListener('click', function(e) {
121
129
  // handle click if not Firefox (Firefox supports this feature natively)
122
130
  if (typeof InstallTrigger === 'undefined') {
123
131
  // get click position
124
- let clickY = (e.pageY - $(this).offset().top);
125
- let height = parseFloat( $(this).height() );
132
+ let clickY = e.pageY - (this.getBoundingClientRect().top + window.scrollY);
133
+ let height = this.offsetHeight;
126
134
 
127
135
  // avoids interference with controls
128
136
  if (clickY > 0.82*height) return;
@@ -1,8 +1,10 @@
1
1
  require('./string');
2
2
  const { FlashMessage } = require('./flash_message');
3
+ const { toEl } = require('./util');
3
4
 
4
5
  class MultiFilesInput {
5
6
  static init(fileInput, setFilesList, nbMaxFiles, maxFileSize) {
7
+ fileInput = toEl(fileInput);
6
8
  let filesList = [];
7
9
  const formGroup = fileInput.closest('.form-group');
8
10
 
@@ -1,3 +1,4 @@
1
+ const { toEl } = require('./util');
1
2
 
2
3
  class MultipleActionInTable {
3
4
 
@@ -5,6 +6,7 @@ class MultipleActionInTable {
5
6
  // Idempotent : sans effet si les colonnes existent déjà.
6
7
  // Doit être appelé AVANT l'initialisation DataTable.
7
8
  static initCols(table, cellSelector = 'select') {
9
+ table = toEl(table);
8
10
  if (!table.classList.contains('table-action_multiple')) {
9
11
  return;
10
12
  }
@@ -26,6 +28,7 @@ class MultipleActionInTable {
26
28
  // Initialise les colonnes (via initCols) puis branche les event handlers.
27
29
  // Peut être appelé après l'initialisation DataTable.
28
30
  static init(table, options = {}) {
31
+ table = toEl(table);
29
32
  const { cellSelector = 'select', imgArrow = '' } = options;
30
33
 
31
34
  if (!table.classList.contains('table-action_multiple')) {
@@ -141,6 +144,7 @@ class MultipleActionInTable {
141
144
  class MultipleActionInDivList {
142
145
  // init checkbox
143
146
  static init(contentDiv, options = {}) {
147
+ contentDiv = toEl(contentDiv);
144
148
  const { imgArrow = '' } = options;
145
149
 
146
150
  let buttonsDiv = MultipleActionInDivList.getButtonsDiv(contentDiv);
@@ -254,115 +258,4 @@ class MultipleActionInDivList {
254
258
  }
255
259
  }
256
260
 
257
- module.exports = { MultipleActionInTable, MultipleActionInDivList };
258
-
259
- /*
260
- // init checkbox
261
- function initTableActionMultiple(table) {
262
- if (!table.hasClass('table-action_multiple')) {
263
- return;
264
- }
265
-
266
- var divBtn = tableActionMultipleGetDivBtn(table);
267
- if (divBtn == null) {
268
- return;
269
- }
270
-
271
- if (table.find('thead tr th[data-key="select"]').length === 0) {
272
- table.find('thead tr').prepend($('<th class="select no-sort" data-key="select"></th>'));
273
- }
274
- table.find('tbody tr:not(.no_items)').each(function(idx, tr) {
275
- if ($(tr).find('td.select').length === 0) {
276
- $(tr).prepend($('<td class="select"><input type="checkbox" class="action_multiple_checkbox" name="'+$(tr).data('action_multiple_input_name')+'" value="'+$(tr).data('action_multiple_item_id')+'"></td>'));
277
- }
278
- });
279
-
280
- table.find('input.action_multiple_checkbox').each(function(idx, el) {
281
- var th = $(el).closest('table').find('thead tr th').first();
282
- if (th.find('input').length === 0) {
283
- // console.log(th);
284
- th.html('<input type="checkbox" class="action_multiple_check_all" />');
285
- // th.html('Coucou');
286
- }
287
- });
288
-
289
- table.find('input.action_multiple_checkbox').change(function() {
290
- majCheckbox(table);
291
- });
292
-
293
- table.find('input.action_multiple_check_all').off('click').click(function() {
294
- var table = $(this).closest('table');
295
- var checkbox = table.find('input.action_multiple_checkbox');
296
- var checkboxChecked = table.find('input.action_multiple_checkbox:checked');
297
- if (checkbox.length === checkboxChecked.length) {
298
- checkbox.prop('checked', false);
299
- }
300
- else {
301
- checkbox.prop('checked', true);
302
- }
303
- majCheckbox(table);
304
- });
305
- }
306
-
307
- function majCheckbox(table) {
308
- showButtonsAction(table);
309
-
310
- var allCheckbox = table.find('input.action_multiple_checkbox');
311
- var allCheckboxChecked = table.find('input.action_multiple_checkbox:checked');
312
- var checkboxSelectAll = table.find('thead tr th input.action_multiple_check_all');
313
- if (allCheckbox.length === allCheckboxChecked.length) {
314
- checkboxSelectAll.prop('checked', true);
315
- }
316
- else {
317
- checkboxSelectAll.prop('checked', false);
318
- }
319
- }
320
-
321
- function tableActionMultipleGetDivBtn(table) {
322
- var divTableResponsive = table.parent();
323
- var divBtn = divTableResponsive.next();
324
- if (divBtn.hasClass('action_multiple_buttons')) {
325
- return divBtn;
326
- }
327
- divBtn = divTableResponsive.parent().parent().parent().next();
328
- if (divBtn.hasClass('action_multiple_buttons')) {
329
- return divBtn;
330
- }
331
- return null;
332
- }
333
-
334
- function showButtonsAction(table) {
335
- var divBtn = tableActionMultipleGetDivBtn(table);
336
- if (divBtn == null) {
337
- return;
338
- }
339
-
340
- // console.log(divBtn);
341
- //var nbItems = $('input[name="' + checkbox.attr('name') + '"]:checked').length;
342
- var nbItems = table.find('input.action_multiple_checkbox:checked').length;
343
-
344
- if (nbItems > 0 && divBtn.is(':hidden')) {
345
- divBtn.removeClass('hide');
346
- }
347
- else if (nbItems === 0 && divBtn.is(':visible')) {
348
- divBtn.addClass('hide');
349
- }
350
-
351
- // affichage aucune action possible si aucun bouton n'est visible
352
- if (divBtn.is(':visible')) {
353
- divBtn.find('span.no_button').remove();
354
- if (divBtn.find('button:visible, a:visible').length === 0) {
355
- divBtn.find('img').after('<span class="no_button"><em>aucune action possible</em></span>');
356
- }
357
- }
358
- }
359
-
360
- $(function() {
361
- $('.action_multiple_buttons').prepend($('<img src="'+ROOT_PATH+DOSSIER_IMAGES+'arrow_ltr.png" alt="" /> &nbsp;'));
362
- $('.action_multiple_buttons').append($('<br/><br/>'));
363
-
364
- $('table.table-action_multiple').each(function(idx, table) {
365
- initTableActionMultiple($(table));
366
- });
367
- });
368
- */
261
+ module.exports = { MultipleActionInTable, MultipleActionInDivList };
@@ -1,4 +1,5 @@
1
1
  const L = require('leaflet');
2
+ const { toEl } = require('./util');
2
3
 
3
4
  /**
4
5
  * https://leafletjs.com/
@@ -8,6 +9,7 @@ const L = require('leaflet');
8
9
  class OpenStreetMap {
9
10
 
10
11
  constructor(mapContainer, options={}) {
12
+ mapContainer = toEl(mapContainer);
11
13
  /*let [lat, lng] = button.data('coordinates').split(',');
12
14
  let map = L.map('modal_map_canvas2').setView([lat, lng], 17);
13
15
 
@@ -27,6 +29,7 @@ class OpenStreetMap {
27
29
  }
28
30
 
29
31
  static createMap(mapContainer, options={}) {
32
+ mapContainer = toEl(mapContainer);
30
33
  if (!mapContainer.length) {
31
34
  return null;
32
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osimatic/helpers-js",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "main": "main.js",
5
5
  "scripts": {
6
6
  "test": "jest",
package/paging.js CHANGED
@@ -2,17 +2,21 @@
2
2
  //Bootstrap class pagination https://getbootstrap.com/docs/3.3/components/#pagination
3
3
 
4
4
  const { UrlAndQueryString } = require('./network');
5
+ const { toEl } = require('./util');
5
6
 
6
7
  class Pagination {
7
8
  static paginateCards(div, nbItemsPerPage) {
9
+ div = toEl(div);
8
10
  Pagination.paginate(div, div.querySelectorAll('.pagination_item'), nbItemsPerPage, null);
9
11
  }
10
12
 
11
13
  static paginateTable(table, select=null) {
14
+ table = toEl(table); select = toEl(select);
12
15
  Pagination.paginate(table, table.querySelectorAll('tbody tr:not(.hide)'), parseInt(table.dataset.max_rows), select);
13
16
  }
14
17
 
15
18
  static paginate(div, items, nbItemsPerPage, select=null, labelDisplayAll=null) {
19
+ div = toEl(div); select = toEl(select);
16
20
  let maxItems = nbItemsPerPage;
17
21
 
18
22
  if (!div) {
package/select_all.js CHANGED
@@ -1,8 +1,11 @@
1
+ const { toEl } = require('./util');
2
+
1
3
  class SelectAll {
2
4
 
3
5
  // Dans un form-group
4
6
 
5
7
  static initLinkInFormGroup(link) {
8
+ link = toEl(link);
6
9
  const linkClone = link.cloneNode(true);
7
10
  link.parentElement.replaceChild(linkClone, link);
8
11
  linkClone.addEventListener('click', function(e) {
@@ -43,6 +46,7 @@ class SelectAll {
43
46
  // Dans tableau
44
47
 
45
48
  static initInTable(table) {
49
+ table = toEl(table);
46
50
  const inputCheckAll = table.querySelector('tr input.check_all');
47
51
  if (!inputCheckAll) {
48
52
  return;
@@ -78,6 +82,7 @@ class SelectAll {
78
82
  // Dans un div
79
83
 
80
84
  static initDiv(contentDiv) {
85
+ contentDiv = toEl(contentDiv);
81
86
  contentDiv.querySelectorAll('input.check_all').forEach(inputCheckAll => {
82
87
  const div = inputCheckAll.closest('div.checkbox_with_check_all');
83
88
 
package/sortable_list.js CHANGED
@@ -1,5 +1,8 @@
1
+ const { toEl } = require('./util');
2
+
1
3
  class SortableList {
2
4
  static init(sortableList, clientYOffset=0) {
5
+ sortableList = toEl(sortableList);
3
6
  sortableList.querySelectorAll('[draggable="true"]').forEach(item => {
4
7
  item.addEventListener('dragstart', () => {
5
8
  // Adding dragging class to an item after a delay
@@ -1,5 +1,35 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
1
4
  const { Chartjs } = require('../chartjs');
2
5
 
6
+ // ─── chart creation helpers ──────────────────────────────────────────────────
7
+
8
+ let mockChartInstance;
9
+
10
+ beforeEach(() => {
11
+ mockChartInstance = { data: null, config: null };
12
+ global.Chart = jest.fn((ctx, config) => {
13
+ mockChartInstance.config = config;
14
+ return mockChartInstance;
15
+ });
16
+ HTMLCanvasElement.prototype.getContext = jest.fn(() => ({}));
17
+ });
18
+
19
+ afterEach(() => {
20
+ delete global.Chart;
21
+ document.body.innerHTML = '';
22
+ jest.restoreAllMocks();
23
+ });
24
+
25
+ function makeCanvas() {
26
+ const canvas = document.createElement('canvas');
27
+ document.body.appendChild(canvas);
28
+ return canvas;
29
+ }
30
+
31
+ // ─── groupByPeriod ───────────────────────────────────────────────────────────
32
+
3
33
  describe('Chartjs', () => {
4
34
  describe('groupByPeriod', () => {
5
35
  test('should group data by day (default)', () => {
@@ -30,8 +60,8 @@ describe('Chartjs', () => {
30
60
 
31
61
  expect(result).toHaveLength(2);
32
62
  expect(result[0].label).toBe('2024-01');
33
- expect(result[0].views).toBe(15); // Average of 10 and 20
34
- expect(result[0].clicks).toBe(6.5); // Average of 5 and 8
63
+ expect(result[0].views).toBe(15);
64
+ expect(result[0].clicks).toBe(6.5);
35
65
  expect(result[1].label).toBe('2024-02');
36
66
  expect(result[1].views).toBe(15);
37
67
  expect(result[1].clicks).toBe(6);
@@ -48,9 +78,7 @@ describe('Chartjs', () => {
48
78
 
49
79
  const result = Chartjs.groupByPeriod(data, 'week', metrics);
50
80
 
51
- // Results should be grouped by weeks
52
81
  expect(result.length).toBeGreaterThan(0);
53
- // Check that labels contain week format (YYYY-SX)
54
82
  result.forEach(item => {
55
83
  expect(item.label).toMatch(/^\d{4}-S\d+$/);
56
84
  });
@@ -79,20 +107,11 @@ describe('Chartjs', () => {
79
107
  const result = Chartjs.groupByPeriod(data, 'day', metrics);
80
108
 
81
109
  expect(result).toHaveLength(1);
82
- expect(result[0]).toEqual({
83
- label: '2024-01-15',
84
- views: 10,
85
- clicks: 5,
86
- conversions: 2
87
- });
110
+ expect(result[0]).toEqual({ label: '2024-01-15', views: 10, clicks: 5, conversions: 2 });
88
111
  });
89
112
 
90
113
  test('should handle empty data', () => {
91
- const data = {};
92
- const metrics = ['views'];
93
-
94
- const result = Chartjs.groupByPeriod(data, 'day', metrics);
95
-
114
+ const result = Chartjs.groupByPeriod({}, 'day', ['views']);
96
115
  expect(result).toEqual([]);
97
116
  });
98
117
 
@@ -102,13 +121,11 @@ describe('Chartjs', () => {
102
121
  '2024-01-15': { score: 200 },
103
122
  '2024-01-20': { score: 300 }
104
123
  };
105
- const metrics = ['score'];
106
-
107
- const result = Chartjs.groupByPeriod(data, 'month', metrics);
124
+ const result = Chartjs.groupByPeriod(data, 'month', ['score']);
108
125
 
109
126
  expect(result).toHaveLength(1);
110
127
  expect(result[0].label).toBe('2024-01');
111
- expect(result[0].score).toBe(200); // Average of 100, 200, 300
128
+ expect(result[0].score).toBe(200);
112
129
  });
113
130
 
114
131
  test('should handle missing metric values', () => {
@@ -116,9 +133,7 @@ describe('Chartjs', () => {
116
133
  '2024-01-15': { views: 10 },
117
134
  '2024-01-16': { views: 20, clicks: 5 }
118
135
  };
119
- const metrics = ['views', 'clicks'];
120
-
121
- const result = Chartjs.groupByPeriod(data, 'day', metrics);
136
+ const result = Chartjs.groupByPeriod(data, 'day', ['views', 'clicks']);
122
137
 
123
138
  expect(result).toHaveLength(2);
124
139
  expect(result[0]).toEqual({ label: '2024-01-15', views: 10, clicks: NaN });
@@ -130,9 +145,7 @@ describe('Chartjs', () => {
130
145
  '2023-12-30': { count: 5 },
131
146
  '2024-01-02': { count: 10 }
132
147
  };
133
- const metrics = ['count'];
134
-
135
- const result = Chartjs.groupByPeriod(data, 'month', metrics);
148
+ const result = Chartjs.groupByPeriod(data, 'month', ['count']);
136
149
 
137
150
  expect(result).toHaveLength(2);
138
151
  expect(result[0].label).toBe('2023-12');
@@ -142,123 +155,126 @@ describe('Chartjs', () => {
142
155
 
143
156
  describe('getAutoGranularity', () => {
144
157
  test('should return day_of_month for data spanning 30 days or less', () => {
145
- const data = {
146
- '2024-01-01': {},
147
- '2024-01-15': {},
148
- '2024-01-30': {}
149
- };
158
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-01-15': {}, '2024-01-30': {} })).toBe('day_of_month');
159
+ });
150
160
 
151
- const result = Chartjs.getAutoGranularity(data);
161
+ test('should return week for data spanning 31-90 days', () => {
162
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-03-15': {} })).toBe('week');
163
+ });
152
164
 
153
- expect(result).toBe('day_of_month');
165
+ test('should return month for data spanning more than 90 days', () => {
166
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-05-01': {} })).toBe('month');
154
167
  });
155
168
 
156
- test('should return week for data spanning 31-90 days', () => {
157
- const data = {
158
- '2024-01-01': {},
159
- '2024-03-15': {} // 74 days
160
- };
169
+ test('should return day_of_month for single day', () => {
170
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {} })).toBe('day_of_month');
171
+ });
161
172
 
162
- const result = Chartjs.getAutoGranularity(data);
173
+ test('should return week for exactly 31 days', () => {
174
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-02-01': {} })).toBe('week');
175
+ });
163
176
 
164
- expect(result).toBe('week');
177
+ test('should return week for exactly 90 days', () => {
178
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-03-31': {} })).toBe('week');
165
179
  });
166
180
 
167
- test('should return month for data spanning more than 90 days', () => {
168
- const data = {
169
- '2024-01-01': {},
170
- '2024-05-01': {} // 121 days
171
- };
181
+ test('should return month for exactly 91 days', () => {
182
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-04-01': {} })).toBe('month');
183
+ });
172
184
 
173
- const result = Chartjs.getAutoGranularity(data);
185
+ test('should handle dates in random order', () => {
186
+ expect(Chartjs.getAutoGranularity({ '2024-03-01': {}, '2024-01-01': {}, '2024-02-01': {} })).toBe('week');
187
+ });
174
188
 
175
- expect(result).toBe('month');
189
+ test('should return month for data spanning a full year', () => {
190
+ expect(Chartjs.getAutoGranularity({ '2024-01-01': {}, '2024-12-31': {} })).toBe('month');
176
191
  });
192
+ });
177
193
 
178
- test('should return day_of_month for single day', () => {
179
- const data = {
180
- '2024-01-01': {}
181
- };
194
+ describe('chart creation', () => {
195
+ const chartData = { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] };
182
196
 
183
- const result = Chartjs.getAutoGranularity(data);
197
+ test('createStackedChart clears div and calls Chart constructor', () => {
198
+ const canvas = makeCanvas();
199
+ canvas.innerHTML = '<span>old</span>';
184
200
 
185
- expect(result).toBe('day_of_month');
186
- });
201
+ Chartjs.createStackedChart(canvas, chartData);
187
202
 
188
- test('should return day_of_month for 2 days', () => {
189
- const data = {
190
- '2024-01-01': {},
191
- '2024-01-02': {}
192
- };
203
+ expect(canvas.innerHTML).toBe('');
204
+ expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
205
+ });
193
206
 
194
- const result = Chartjs.getAutoGranularity(data);
207
+ test('createStackedChart merges user options', () => {
208
+ Chartjs.createStackedChart(makeCanvas(), chartData, null, { options: { responsive: false } });
195
209
 
196
- expect(result).toBe('day_of_month');
210
+ expect(mockChartInstance.config.options.responsive).toBe(false);
197
211
  });
198
212
 
199
- test('should return week for exactly 31 days', () => {
200
- const data = {
201
- '2024-01-01': {},
202
- '2024-02-01': {} // 31 days
203
- };
213
+ test('createBarChart clears div and calls Chart constructor', () => {
214
+ const canvas = makeCanvas();
215
+ canvas.innerHTML = '<span>old</span>';
204
216
 
205
- const result = Chartjs.getAutoGranularity(data);
217
+ Chartjs.createBarChart(canvas, chartData);
206
218
 
207
- expect(result).toBe('week');
219
+ expect(canvas.innerHTML).toBe('');
220
+ expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'bar' }));
208
221
  });
209
222
 
210
- test('should return week for exactly 90 days', () => {
211
- const data = {
212
- '2024-01-01': {},
213
- '2024-03-31': {} // 90 days
214
- };
215
-
216
- const result = Chartjs.getAutoGranularity(data);
223
+ test('createBarChart sets title when provided', () => {
224
+ Chartjs.createBarChart(makeCanvas(), chartData, 'My Title');
217
225
 
218
- expect(result).toBe('week');
226
+ expect(mockChartInstance.config.options.plugins.title.display).toBe(true);
227
+ expect(mockChartInstance.config.options.plugins.title.text).toBe('My Title');
219
228
  });
220
229
 
221
- test('should return month for exactly 91 days', () => {
222
- const data = {
223
- '2024-01-01': {},
224
- '2024-04-01': {} // 91 days
225
- };
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
+ });
226
237
 
227
- const result = Chartjs.getAutoGranularity(data);
238
+ test('createDoughnutChart clears div and calls Chart constructor', () => {
239
+ const canvas = makeCanvas();
240
+ Chartjs.createDoughnutChart(canvas, { labels: ['A'], values: [1], colors: ['#f00'] });
228
241
 
229
- expect(result).toBe('month');
242
+ expect(canvas.innerHTML).toBe('');
243
+ expect(global.Chart).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'doughnut' }));
230
244
  });
231
245
 
232
- test('should handle dates in random order', () => {
233
- const data = {
234
- '2024-03-01': {},
235
- '2024-01-01': {},
236
- '2024-02-01': {}
237
- };
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
+ });
238
251
 
239
- const result = Chartjs.getAutoGranularity(data);
252
+ test('title not displayed when null', () => {
253
+ Chartjs.createBarChart(makeCanvas(), chartData, null);
240
254
 
241
- // Should calculate from first to last date (Jan 1 to Mar 1 = ~59 days)
242
- expect(result).toBe('week');
255
+ expect(mockChartInstance.config.options.plugins.title.display).toBe(false);
243
256
  });
244
257
 
245
- test('should return month for data spanning a full year', () => {
246
- const data = {
247
- '2024-01-01': {},
248
- '2024-12-31': {} // 365 days
249
- };
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 };
250
262
 
251
- const result = Chartjs.getAutoGranularity(data);
263
+ expect(() => Chartjs.createStackedChart(jq, chartData)).not.toThrow();
264
+ expect(global.Chart).toHaveBeenCalled();
265
+ });
252
266
 
253
- expect(result).toBe('month');
254
- });
255
- });
267
+ test('createBarChart accepts jQuery-like object', () => {
268
+ const canvas = makeCanvas();
269
+ const jq = { jquery: '3.6.0', 0: canvas, length: 1 };
256
270
 
257
- describe('Chartjs class structure', () => {
258
- test('should be a class', () => {
259
- expect(typeof Chartjs).toBe('function');
271
+ expect(() => Chartjs.createBarChart(jq, chartData)).not.toThrow();
272
+ expect(global.Chart).toHaveBeenCalled();
273
+ });
260
274
  });
275
+ });
261
276
 
277
+ describe('class structure', () => {
262
278
  test('should have all expected static methods', () => {
263
279
  expect(typeof Chartjs.init).toBe('function');
264
280
  expect(typeof Chartjs.createStackedChart).toBe('function');