@osimatic/helpers-js 1.5.20 → 1.5.22
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/data_table.js +87 -80
- package/form_helper.js +2 -2
- package/index.js +2 -2
- package/media.js +1 -1
- package/open_street_map.js +2 -1
- package/package.json +2 -1
- package/select_all.js +27 -0
- package/tests/chartjs.test.js +174 -147
- package/tests/form_helper.test.js +294 -10
- package/tests/open_street_map.test.js +592 -251
- package/tests/select_all.test.js +48 -0
|
@@ -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
|
|
@@ -909,23 +910,306 @@ describe('FormHelper', () => {
|
|
|
909
910
|
});
|
|
910
911
|
|
|
911
912
|
describe('ArrayField', () => {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
913
|
+
let container;
|
|
914
|
+
|
|
915
|
+
beforeEach(() => {
|
|
916
|
+
container = document.createElement('div');
|
|
917
|
+
document.body.appendChild(container);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
afterEach(() => {
|
|
921
|
+
document.body.innerHTML = '';
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
function makeOptions(overrides = {}) {
|
|
925
|
+
return Object.assign({
|
|
926
|
+
entering_field_in_table: false,
|
|
927
|
+
add_one_button_enabled: true,
|
|
928
|
+
add_multi_button_enabled: false,
|
|
929
|
+
input_name: 'items[]',
|
|
930
|
+
item_name: 'Item',
|
|
931
|
+
}, overrides);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// jsdom applies HTML5 foster-parenting: <tr> inside <div>.innerHTML gets stripped.
|
|
935
|
+
// ArrayField.init falls back to cloneNode when a .base template row exists,
|
|
936
|
+
// so we pre-populate the container with a base row to make row tests work.
|
|
937
|
+
function addBaseRow(opts = {}) {
|
|
938
|
+
const enteringInTable = opts.entering_field_in_table ?? false;
|
|
939
|
+
const inputName = opts.input_name ?? 'items[]';
|
|
940
|
+
let tdContent, links;
|
|
941
|
+
if (enteringInTable) {
|
|
942
|
+
tdContent = `<input type="text" name="${inputName}" class="form-control">`;
|
|
943
|
+
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>';
|
|
944
|
+
} else {
|
|
945
|
+
tdContent = `<input type="hidden" name="${inputName}"> <span class="value"></span>`;
|
|
946
|
+
links = '<a href="#" class="remove btn btn-sm btn-danger ms-1"></a>';
|
|
947
|
+
}
|
|
948
|
+
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>`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
test('creates table and list_empty if not present', () => {
|
|
952
|
+
ArrayField.init(container, [], makeOptions());
|
|
953
|
+
expect(container.querySelector('table')).not.toBeNull();
|
|
954
|
+
expect(container.querySelector('.list_empty')).not.toBeNull();
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('shows list_empty and hides table when no items', () => {
|
|
958
|
+
ArrayField.init(container, [], makeOptions());
|
|
959
|
+
expect(container.querySelector('.list_empty').classList.contains('hide')).toBe(false);
|
|
960
|
+
expect(container.querySelector('table').classList.contains('hide')).toBe(true);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('populates default values as rows', () => {
|
|
964
|
+
addBaseRow();
|
|
965
|
+
ArrayField.init(container, ['foo', 'bar'], makeOptions());
|
|
966
|
+
const rows = container.querySelectorAll('table tbody tr:not(.base)');
|
|
967
|
+
expect(rows).toHaveLength(2);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test('table is visible when items are present', () => {
|
|
971
|
+
addBaseRow();
|
|
972
|
+
ArrayField.init(container, ['item1'], makeOptions());
|
|
973
|
+
expect(container.querySelector('table').classList.contains('hide')).toBe(false);
|
|
974
|
+
expect(container.querySelector('.list_empty').classList.contains('hide')).toBe(true);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test('row contains hidden input and span.value for entering_field_in_table=false', () => {
|
|
978
|
+
addBaseRow({ entering_field_in_table: false });
|
|
979
|
+
ArrayField.init(container, ['hello'], makeOptions({ entering_field_in_table: false }));
|
|
980
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
981
|
+
expect(row.querySelector('input[type="hidden"]')).not.toBeNull();
|
|
982
|
+
expect(row.querySelector('span.value')).not.toBeNull();
|
|
983
|
+
expect(row.querySelector('input[type="hidden"]').value).toBe('hello');
|
|
984
|
+
expect(row.querySelector('span.value').textContent).toBe('hello');
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test('row contains text input for entering_field_in_table=true', () => {
|
|
988
|
+
addBaseRow({ entering_field_in_table: true });
|
|
989
|
+
ArrayField.init(container, ['hello'], makeOptions({ entering_field_in_table: true, nb_min_lines: 0 }));
|
|
990
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
991
|
+
expect(row.querySelector('input[type="text"]')).not.toBeNull();
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test('creates add_one button when add_one_button_enabled', () => {
|
|
995
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
996
|
+
expect(container.querySelector('a.add_one')).not.toBeNull();
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('does not create add_one button when disabled', () => {
|
|
1000
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: false }));
|
|
1001
|
+
expect(container.querySelector('a.add_one')).toBeNull();
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test('clicking add_one shows item_add_one div', () => {
|
|
1005
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1006
|
+
container.querySelector('a.add_one').click();
|
|
1007
|
+
expect(container.querySelector('.item_add_one').classList.contains('hide')).toBe(false);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test('clicking cancel in add_one hides item_add_one div', () => {
|
|
1011
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1012
|
+
container.querySelector('a.add_one').click();
|
|
1013
|
+
container.querySelector('.item_add_one a.cancel').click();
|
|
1014
|
+
expect(container.querySelector('.item_add_one').classList.contains('hide')).toBe(true);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test('clicking add in item_add_one adds a new row', () => {
|
|
1018
|
+
addBaseRow();
|
|
1019
|
+
ArrayField.init(container, [], makeOptions({ add_one_button_enabled: true }));
|
|
1020
|
+
container.querySelector('a.add_one').click();
|
|
1021
|
+
container.querySelector('.item_add_one input.form-control').value = 'newitem';
|
|
1022
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1023
|
+
const rows = container.querySelectorAll('table tbody tr:not(.base)');
|
|
1024
|
+
expect(rows).toHaveLength(1);
|
|
1025
|
+
expect(rows[0].querySelector('span.value').textContent).toBe('newitem');
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test('remove button removes a row', () => {
|
|
1029
|
+
addBaseRow();
|
|
1030
|
+
ArrayField.init(container, ['a', 'b'], makeOptions());
|
|
1031
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(2);
|
|
1032
|
+
container.querySelector('table tbody tr:not(.base) a.remove').click();
|
|
1033
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(1);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
test('calls update_list_callback on changes', () => {
|
|
1037
|
+
addBaseRow();
|
|
1038
|
+
const cb = jest.fn();
|
|
1039
|
+
ArrayField.init(container, ['x'], makeOptions({ update_list_callback: cb }));
|
|
1040
|
+
expect(cb).toHaveBeenCalled();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test('calls init_callback with container after init', () => {
|
|
1044
|
+
const cb = jest.fn();
|
|
1045
|
+
ArrayField.init(container, [], makeOptions({ init_callback: cb }));
|
|
1046
|
+
expect(cb).toHaveBeenCalledWith(container, expect.any(Function), expect.any(Function));
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test('uses custom list_empty_text', () => {
|
|
1050
|
+
ArrayField.init(container, [], makeOptions({ list_empty_text: 'Nothing here' }));
|
|
1051
|
+
expect(container.querySelector('.list_empty').textContent).toBe('Nothing here');
|
|
915
1052
|
});
|
|
916
1053
|
|
|
917
|
-
test('
|
|
918
|
-
|
|
1054
|
+
test('calls get_errors_callback and shows errors on invalid input', () => {
|
|
1055
|
+
addBaseRow();
|
|
1056
|
+
ArrayField.init(container, [], makeOptions({
|
|
1057
|
+
add_one_button_enabled: true,
|
|
1058
|
+
get_errors_callback: () => ['Invalid value'],
|
|
1059
|
+
}));
|
|
1060
|
+
container.querySelector('a.add_one').click();
|
|
1061
|
+
container.querySelector('.item_add_one input.form-control').value = 'bad';
|
|
1062
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1063
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(0);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test('does not add duplicate items', () => {
|
|
1067
|
+
addBaseRow();
|
|
1068
|
+
ArrayField.init(container, ['dup'], makeOptions({ add_one_button_enabled: true }));
|
|
1069
|
+
container.querySelector('a.add_one').click();
|
|
1070
|
+
container.querySelector('.item_add_one input.form-control').value = 'dup';
|
|
1071
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1072
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(1);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test('applies nb_max_lines limit: disables add button when reached', () => {
|
|
1076
|
+
addBaseRow({ entering_field_in_table: true });
|
|
1077
|
+
ArrayField.init(container, ['a', 'b'], makeOptions({
|
|
1078
|
+
entering_field_in_table: true,
|
|
1079
|
+
nb_max_lines: 2,
|
|
1080
|
+
nb_min_lines: 0,
|
|
1081
|
+
}));
|
|
1082
|
+
const addLinks = container.querySelectorAll('table tbody tr:not(.base) a.add');
|
|
1083
|
+
addLinks.forEach(a => expect(a.classList.contains('disabled')).toBe(true));
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
test('creates add_multi button when add_multi_button_enabled', () => {
|
|
1087
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1088
|
+
expect(container.querySelector('a.add_multi')).not.toBeNull();
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
test('clicking add_multi shows item_add_multi div', () => {
|
|
1092
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1093
|
+
container.querySelector('a.add_multi').click();
|
|
1094
|
+
expect(container.querySelector('.item_add_multi').classList.contains('hide')).toBe(false);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
test('clicking cancel in add_multi hides item_add_multi', () => {
|
|
1098
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1099
|
+
container.querySelector('a.add_multi').click();
|
|
1100
|
+
container.querySelector('.item_add_multi a.cancel').click();
|
|
1101
|
+
expect(container.querySelector('.item_add_multi').classList.contains('hide')).toBe(true);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test('add_multi adds multiple items from textarea', () => {
|
|
1105
|
+
addBaseRow();
|
|
1106
|
+
ArrayField.init(container, [], makeOptions({ add_multi_button_enabled: true, add_one_button_enabled: false }));
|
|
1107
|
+
container.querySelector('a.add_multi').click();
|
|
1108
|
+
container.querySelector('.item_add_multi textarea').value = 'alpha\nbeta\ngamma';
|
|
1109
|
+
container.querySelector('.item_add_multi a.add').click();
|
|
1110
|
+
expect(container.querySelectorAll('table tbody tr:not(.base)')).toHaveLength(3);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
test('applies format_entered_value_callback on add', () => {
|
|
1114
|
+
addBaseRow();
|
|
1115
|
+
ArrayField.init(container, [], makeOptions({
|
|
1116
|
+
add_one_button_enabled: true,
|
|
1117
|
+
format_entered_value_callback: v => v.toUpperCase(),
|
|
1118
|
+
}));
|
|
1119
|
+
container.querySelector('a.add_one').click();
|
|
1120
|
+
container.querySelector('.item_add_one input.form-control').value = 'hello';
|
|
1121
|
+
container.querySelector('.item_add_one a.add').click();
|
|
1122
|
+
const row = container.querySelector('table tbody tr:not(.base)');
|
|
1123
|
+
expect(row.querySelector('span.value').textContent).toBe('HELLO');
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test('entering_field_in_table: remove disabled when only 1 row remains', () => {
|
|
1127
|
+
addBaseRow({ entering_field_in_table: true });
|
|
1128
|
+
ArrayField.init(container, ['only'], makeOptions({ entering_field_in_table: true, nb_min_lines: 0 }));
|
|
1129
|
+
const removeLink = container.querySelector('table tbody tr:not(.base) a.remove');
|
|
1130
|
+
expect(removeLink.classList.contains('disabled')).toBe(true);
|
|
919
1131
|
});
|
|
920
1132
|
});
|
|
921
1133
|
|
|
922
1134
|
describe('EditValue', () => {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1135
|
+
let valueDiv, parent;
|
|
1136
|
+
|
|
1137
|
+
beforeEach(() => {
|
|
1138
|
+
parent = document.createElement('div');
|
|
1139
|
+
valueDiv = document.createElement('span');
|
|
1140
|
+
valueDiv.textContent = 'original';
|
|
1141
|
+
parent.appendChild(valueDiv);
|
|
1142
|
+
document.body.appendChild(parent);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
afterEach(() => {
|
|
1146
|
+
document.body.innerHTML = '';
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('appends a pencil link next to valueDiv', () => {
|
|
1150
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1151
|
+
expect(parent.querySelector('a')).not.toBeNull();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
test('clicking pencil link hides spans and links and shows a form', () => {
|
|
1155
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1156
|
+
parent.querySelector('a').click();
|
|
1157
|
+
expect(parent.querySelector('form')).not.toBeNull();
|
|
1158
|
+
expect(valueDiv.classList.contains('hide')).toBe(true);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
test('form contains input pre-filled with current text', () => {
|
|
1162
|
+
valueDiv.textContent = 'current value';
|
|
1163
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1164
|
+
parent.querySelector('a').click();
|
|
1165
|
+
const input = parent.querySelector('form input');
|
|
1166
|
+
expect(input.value).toBe('current value');
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
test('form uses data-value attribute when present', () => {
|
|
1170
|
+
valueDiv.dataset.value = 'raw-value';
|
|
1171
|
+
valueDiv.textContent = 'Formatted value';
|
|
1172
|
+
EditValue.init(valueDiv, jest.fn());
|
|
1173
|
+
parent.querySelector('a').click();
|
|
1174
|
+
expect(parent.querySelector('form input').value).toBe('raw-value');
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test('calls onSubmitCallback with new value when submit button clicked', () => {
|
|
1178
|
+
const cb = jest.fn();
|
|
1179
|
+
EditValue.init(valueDiv, cb);
|
|
1180
|
+
parent.querySelector('a').click();
|
|
1181
|
+
parent.querySelector('form input').value = 'new val';
|
|
1182
|
+
parent.querySelector('form button').click();
|
|
1183
|
+
expect(cb).toHaveBeenCalledWith('new val', parent, expect.any(Function));
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
test('on success callback updates span value', () => {
|
|
1187
|
+
let capturedCallback;
|
|
1188
|
+
const cb = jest.fn((newVal, par, done) => { capturedCallback = done; });
|
|
1189
|
+
EditValue.init(valueDiv, cb);
|
|
1190
|
+
parent.querySelector('a').click();
|
|
1191
|
+
parent.querySelector('form input').value = 'updated';
|
|
1192
|
+
parent.querySelector('form button').click();
|
|
1193
|
+
capturedCallback(true);
|
|
1194
|
+
expect(valueDiv.textContent).toBe('updated');
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
test('on failure callback does not update span value', () => {
|
|
1198
|
+
let capturedCallback;
|
|
1199
|
+
const cb = jest.fn((newVal, par, done) => { capturedCallback = done; });
|
|
1200
|
+
EditValue.init(valueDiv, cb);
|
|
1201
|
+
parent.querySelector('a').click();
|
|
1202
|
+
parent.querySelector('form input').value = 'updated';
|
|
1203
|
+
parent.querySelector('form button').click();
|
|
1204
|
+
capturedCallback(false);
|
|
1205
|
+
expect(valueDiv.textContent).toBe('original');
|
|
926
1206
|
});
|
|
927
1207
|
|
|
928
|
-
test('
|
|
929
|
-
|
|
1208
|
+
test('uses getInputCallback for custom input element', () => {
|
|
1209
|
+
const getInput = jest.fn(() => '<select><option value="x">X</option></select>');
|
|
1210
|
+
EditValue.init(valueDiv, jest.fn(), getInput);
|
|
1211
|
+
parent.querySelector('a').click();
|
|
1212
|
+
expect(parent.querySelector('form select')).not.toBeNull();
|
|
1213
|
+
expect(getInput).toHaveBeenCalled();
|
|
930
1214
|
});
|
|
931
1215
|
});
|