@yuneta/gobj-ui 7.26.32 → 7.26.34

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.
@@ -10415,6 +10415,7 @@ function template2columns(gobj, columns, template, sub_elements) {
10415
10415
  else return value;
10416
10416
  };
10417
10417
  if (field_desc.is_required) validator.push("required");
10418
+ if ((0, _yuneta_gobj_js.is_object)(value) && value.validator) validator.push(...Array.isArray(value.validator) ? value.validator : [value.validator]);
10418
10419
  switch (field_desc.type) {
10419
10420
  case "object":
10420
10421
  case "dict": break;
@@ -10631,12 +10632,20 @@ function validate_form(gobj, $form) {
10631
10632
  mark_field_validity($editor);
10632
10633
  }
10633
10634
  });
10635
+ let $first_bad = null;
10636
+ function table_valid(tabulator) {
10637
+ let r = tabulator.validate();
10638
+ if (r === true) return true;
10639
+ if (!$first_bad && Array.isArray(r) && r.length > 0) $first_bad = r[0].getElement();
10640
+ return false;
10641
+ }
10634
10642
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
10635
- if (!$table.tabulator.validate()) validated = false;
10643
+ if (!table_valid($table.tabulator)) validated = false;
10636
10644
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
10637
- if (!$table_nested.tabulator.validate()) validated = false;
10645
+ if (!table_valid($table_nested.tabulator)) validated = false;
10638
10646
  });
10639
10647
  });
10648
+ if ($first_bad) $first_bad.scrollIntoView({ block: "nearest" });
10640
10649
  return validated;
10641
10650
  }
10642
10651
  /************************************************************
@@ -10711,7 +10720,9 @@ function get_form_values(gobj, $form) {
10711
10720
  else value = value_.text;
10712
10721
  }
10713
10722
  break;
10714
- case "fieldset": break;
10723
+ case "fieldset":
10724
+ value = {};
10725
+ break;
10715
10726
  case "table":
10716
10727
  let template = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template");
10717
10728
  if (!(0, _yuneta_gobj_js.empty_string)(name) && (0, _yuneta_gobj_js.kw_has_key)(template, name)) {
@@ -10733,7 +10744,7 @@ function get_form_values(gobj, $form) {
10733
10744
  break;
10734
10745
  default: (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: $form type unknown: '%s'", (0, _yuneta_gobj_js.gobj_short_name)(gobj), tag));
10735
10746
  }
10736
- value = form_value_2_treedb_value(gobj, field_desc, value);
10747
+ if (tag !== "fieldset") value = form_value_2_treedb_value(gobj, field_desc, value);
10737
10748
  let path;
10738
10749
  if ((0, _yuneta_gobj_js.empty_string)(prefix)) path = name;
10739
10750
  else path = prefix + "`" + name;
@@ -10754,7 +10765,13 @@ function clear_data(gobj, $form) {
10754
10765
  } else if ($input.type === "checkbox") $input.checked = false;
10755
10766
  else if ($input.type === "radio") $input.checked = false;
10756
10767
  else if ($input.type === "datetime-local") $input.value = null;
10757
- else if ($input.type === "select-multiple") $input.tom_select.clear();
10768
+ else if ($input.type === "color") {
10769
+ $input.value = "#000000";
10770
+ if ($input.yui_color_value) {
10771
+ $input.yui_color_value.textContent = $input.value;
10772
+ $input.yui_color_value.title = $input.value;
10773
+ }
10774
+ } else if ($input.type === "select-multiple") $input.tom_select.clear();
10758
10775
  else {
10759
10776
  $input.value = null;
10760
10777
  if ($input.yui_svg_preview) $input.yui_svg_preview();
@@ -10410,6 +10410,7 @@ function template2columns(gobj, columns, template, sub_elements) {
10410
10410
  else return value;
10411
10411
  };
10412
10412
  if (field_desc.is_required) validator.push("required");
10413
+ if (is_object(value) && value.validator) validator.push(...Array.isArray(value.validator) ? value.validator : [value.validator]);
10413
10414
  switch (field_desc.type) {
10414
10415
  case "object":
10415
10416
  case "dict": break;
@@ -10626,12 +10627,20 @@ function validate_form(gobj, $form) {
10626
10627
  mark_field_validity($editor);
10627
10628
  }
10628
10629
  });
10630
+ let $first_bad = null;
10631
+ function table_valid(tabulator) {
10632
+ let r = tabulator.validate();
10633
+ if (r === true) return true;
10634
+ if (!$first_bad && Array.isArray(r) && r.length > 0) $first_bad = r[0].getElement();
10635
+ return false;
10636
+ }
10629
10637
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
10630
- if (!$table.tabulator.validate()) validated = false;
10638
+ if (!table_valid($table.tabulator)) validated = false;
10631
10639
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
10632
- if (!$table_nested.tabulator.validate()) validated = false;
10640
+ if (!table_valid($table_nested.tabulator)) validated = false;
10633
10641
  });
10634
10642
  });
10643
+ if ($first_bad) $first_bad.scrollIntoView({ block: "nearest" });
10635
10644
  return validated;
10636
10645
  }
10637
10646
  /************************************************************
@@ -10706,7 +10715,9 @@ function get_form_values(gobj, $form) {
10706
10715
  else value = value_.text;
10707
10716
  }
10708
10717
  break;
10709
- case "fieldset": break;
10718
+ case "fieldset":
10719
+ value = {};
10720
+ break;
10710
10721
  case "table":
10711
10722
  let template = gobj_read_attr(gobj, "template");
10712
10723
  if (!empty_string(name) && kw_has_key(template, name)) {
@@ -10728,7 +10739,7 @@ function get_form_values(gobj, $form) {
10728
10739
  break;
10729
10740
  default: log_error(sprintf("%s: $form type unknown: '%s'", gobj_short_name(gobj), tag));
10730
10741
  }
10731
- value = form_value_2_treedb_value(gobj, field_desc, value);
10742
+ if (tag !== "fieldset") value = form_value_2_treedb_value(gobj, field_desc, value);
10732
10743
  let path;
10733
10744
  if (empty_string(prefix)) path = name;
10734
10745
  else path = prefix + "`" + name;
@@ -10749,7 +10760,13 @@ function clear_data(gobj, $form) {
10749
10760
  } else if ($input.type === "checkbox") $input.checked = false;
10750
10761
  else if ($input.type === "radio") $input.checked = false;
10751
10762
  else if ($input.type === "datetime-local") $input.value = null;
10752
- else if ($input.type === "select-multiple") $input.tom_select.clear();
10763
+ else if ($input.type === "color") {
10764
+ $input.value = "#000000";
10765
+ if ($input.yui_color_value) {
10766
+ $input.yui_color_value.textContent = $input.value;
10767
+ $input.yui_color_value.title = $input.value;
10768
+ }
10769
+ } else if ($input.type === "select-multiple") $input.tom_select.clear();
10753
10770
  else {
10754
10771
  $input.value = null;
10755
10772
  if ($input.yui_svg_preview) $input.yui_svg_preview();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.32",
3
+ "version": "7.26.34",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_form.js CHANGED
@@ -2273,6 +2273,15 @@ function template2columns(gobj, columns, template, sub_elements)
2273
2273
  validator.push("required");
2274
2274
  }
2275
2275
 
2276
+ /* What a host knows and a schema cannot say: a template item may
2277
+ * carry Tabulator validators of its own -- a validator's name, or
2278
+ * a function of (cell, value, parameters) -- that its column runs
2279
+ * besides `required`. A template that comes as json carries none;
2280
+ * the host adds them to its copy before handing it on. */
2281
+ if(is_object(value) && value.validator) {
2282
+ validator.push(...(Array.isArray(value.validator)? value.validator : [value.validator]));
2283
+ }
2284
+
2276
2285
  // TODO add the validators needed, like validator.push("integer");
2277
2286
 
2278
2287
  switch(field_desc.type) {
@@ -2658,18 +2667,38 @@ function validate_form(gobj, $form)
2658
2667
  }
2659
2668
  });
2660
2669
 
2670
+ /* Tabulator's validate() answers `true`, or the LIST of the cells that
2671
+ * failed -- and a list is truthy, so `!validate()` never refused a
2672
+ * table: a cell left empty in a `required` column was saved. Tabulator
2673
+ * marks every bad cell; the first one is brought into view. */
2674
+ let $first_bad = null;
2675
+ function table_valid(tabulator) {
2676
+ let r = tabulator.validate();
2677
+ if(r === true) {
2678
+ return true;
2679
+ }
2680
+ if(!$first_bad && Array.isArray(r) && r.length > 0) {
2681
+ $first_bad = r[0].getElement();
2682
+ }
2683
+ return false;
2684
+ }
2685
+
2661
2686
  $form.querySelectorAll('.yui-tabulator-table').forEach($table => {
2662
- if(!$table.tabulator.validate()) {
2687
+ if(!table_valid($table.tabulator)) {
2663
2688
  validated = false;
2664
2689
  }
2665
2690
 
2666
2691
  $table.querySelectorAll('.yui-tabulator-table-nested').forEach($table_nested => {
2667
- if(!$table_nested.tabulator.validate()) {
2692
+ if(!table_valid($table_nested.tabulator)) {
2668
2693
  validated = false;
2669
2694
  }
2670
2695
  });
2671
2696
  });
2672
2697
 
2698
+ if($first_bad) {
2699
+ $first_bad.scrollIntoView({block: "nearest"});
2700
+ }
2701
+
2673
2702
  return validated;
2674
2703
  }
2675
2704
 
@@ -2767,10 +2796,13 @@ function get_form_values(gobj, $form)
2767
2796
  break;
2768
2797
 
2769
2798
  case "fieldset":
2770
- /*
2771
- * HACK: really this is the path of the value.
2772
- * We are navigating by a tree data structure; this is one node, not a leaf.
2773
- */
2799
+ /* A NODE of the template, not a leaf: its value is the
2800
+ * dict its own fields fill, and they come after it in the
2801
+ * document. No json anybody typed, so it skips the
2802
+ * conversion below, which handed its null on (7.26.8 to
2803
+ * 7.26.32): the fields found no dict to write into and the
2804
+ * save stored the node as null. */
2805
+ value = {};
2774
2806
  break;
2775
2807
 
2776
2808
  case "table":
@@ -2811,7 +2843,9 @@ function get_form_values(gobj, $form)
2811
2843
  break;
2812
2844
  }
2813
2845
 
2814
- value = form_value_2_treedb_value(gobj, field_desc, value);
2846
+ if(tag !== "fieldset") {
2847
+ value = form_value_2_treedb_value(gobj, field_desc, value);
2848
+ }
2815
2849
  let path;
2816
2850
  if(empty_string(prefix)) {
2817
2851
  path = name;
@@ -2846,6 +2880,18 @@ function clear_data(gobj, $form)
2846
2880
  } else if($input.type === 'datetime-local') { /* time */
2847
2881
  $input.value = null;
2848
2882
 
2883
+ } else if($input.type === 'color') {
2884
+ /* A colour input holds a colour: the "" a null is written
2885
+ * as is refused, the browser keeps its black, and Chrome
2886
+ * says so on the console, one line per field each time
2887
+ * the form is filled. Black, then, and the readout beside
2888
+ * the swatch told (writing `.value` fires nothing). */
2889
+ $input.value = "#000000";
2890
+ if($input.yui_color_value) {
2891
+ $input.yui_color_value.textContent = $input.value;
2892
+ $input.yui_color_value.title = $input.value;
2893
+ }
2894
+
2849
2895
  } else if($input.type === 'select-multiple') {
2850
2896
  $input.tom_select.clear();
2851
2897
  } else {
@@ -0,0 +1,192 @@
1
+ /***********************************************************************
2
+ * form_table_validator.wiring.test.js
3
+ *
4
+ * A table field of a C_YUI_FORM ('exec' mode) and its validators.
5
+ *
6
+ * - A template item may carry Tabulator validators of its own
7
+ * (`validator`: a name or a function), which its column runs
8
+ * besides `required`: what a host knows and a schema cannot say
9
+ * (estadodelaire refuses a trigger value that its measure can
10
+ * never match).
11
+ * - A table that does not validate refuses the save. Tabulator's
12
+ * validate() answers `true` or the LIST of the bad cells, and
13
+ * validate_form() asked `!validate()`, which a list never is: a
14
+ * `required` cell left empty was saved.
15
+ *
16
+ * Driven through the real gclass on a document double, with a
17
+ * Tabulator double that keeps its settings and answers validate()
18
+ * as told.
19
+ *
20
+ * Copyright (c) 2026, ArtGins.
21
+ * All Rights Reserved.
22
+ ***********************************************************************/
23
+ import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
24
+ import {install_dom_double} from "../test/dom_double.js";
25
+
26
+ install_dom_double();
27
+
28
+ if(typeof globalThis.ResizeObserver === "undefined") {
29
+ globalThis.ResizeObserver = class {
30
+ observe() {}
31
+ unobserve() {}
32
+ disconnect() {}
33
+ };
34
+ }
35
+
36
+ const tables = vi.hoisted(() => []);
37
+ vi.mock("tabulator-tables", () => {
38
+ class FakeTabulator {
39
+ constructor($el, settings) {
40
+ this.$el = $el;
41
+ this.settings = settings;
42
+ this.externalEvents = {events: {}};
43
+ this.initialized = true;
44
+ this.answer = true; // what validate() says
45
+ this.validated = 0;
46
+ tables.push(this);
47
+ return new Proxy(this, {
48
+ get(target, prop) {
49
+ if(prop in target) {
50
+ return target[prop];
51
+ }
52
+ return () => [];
53
+ }
54
+ });
55
+ }
56
+ on(ev, fn) {
57
+ (this.externalEvents.events[ev] = this.externalEvents.events[ev] || []).push(fn);
58
+ }
59
+ off(ev) {
60
+ delete this.externalEvents.events[ev];
61
+ }
62
+ validate() {
63
+ this.validated++;
64
+ return this.answer;
65
+ }
66
+ getRows() {
67
+ return [];
68
+ }
69
+ destroy() {
70
+ }
71
+ }
72
+ return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
73
+ });
74
+
75
+ const {
76
+ SDATA_END, gclass_create,
77
+ gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
78
+ gobj_start, gobj_send_event,
79
+ set_log_callback,
80
+ } = await import("@yuneta/gobj-js");
81
+ const {register_c_yui_form} = await import("./c_yui_form.js");
82
+
83
+ const saved = [];
84
+ let yuno = null;
85
+
86
+ beforeAll(async () => {
87
+ const i18next = (await import("i18next")).default;
88
+ await i18next.init({lng: "en", resources: {}});
89
+ gobj_start_up(null, null, null, null, null, null, null);
90
+ set_log_callback(() => {});
91
+ gclass_create("C_TEST_FORM_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
92
+ gclass_create(
93
+ "C_TEST_FORM_APP",
94
+ [["EV_SAVE_RECORD", 0], ["EV_RECORD_CHANGED", 0]],
95
+ [["ST_IDLE", [
96
+ ["EV_SAVE_RECORD", (gobj, event, kw) => {
97
+ saved.push(kw);
98
+ return 0;
99
+ }, null],
100
+ ["EV_RECORD_CHANGED", () => 0, null],
101
+ ]]],
102
+ {}, 0, [SDATA_END()], {}, 0, 0, 0, 0
103
+ );
104
+ register_c_yui_form();
105
+ yuno = gobj_create_yuno("form_validator_yuno", "C_TEST_FORM_HOST", {});
106
+ gobj_start(yuno);
107
+ });
108
+
109
+ beforeEach(() => {
110
+ saved.length = 0;
111
+ tables.length = 0;
112
+ });
113
+
114
+ let seq = 0;
115
+
116
+ /* The alarms of an estadodelaire device, cut down: a table whose
117
+ * `value` column carries a validator the host added. */
118
+ function build(validator)
119
+ {
120
+ const value = {
121
+ type: "string",
122
+ flag: ["writable", "persistent"]
123
+ };
124
+ if(validator) {
125
+ value.validator = validator;
126
+ }
127
+ const app = gobj_create_pure_child(`vapp${++seq}`, "C_TEST_FORM_APP", {}, yuno);
128
+ gobj_start(app);
129
+ const form = gobj_create_pure_child(`vform${seq}`, "C_YUI_FORM", {
130
+ template: {
131
+ triggers: {
132
+ type: "array",
133
+ flag: ["persistent", "writable", "table"],
134
+ table: {
135
+ id: {
136
+ type: "string",
137
+ flag: ["writable", "persistent", "required"]
138
+ },
139
+ value: value
140
+ }
141
+ }
142
+ },
143
+ record: {triggers: []},
144
+ editable: true,
145
+ }, app);
146
+ gobj_start(form);
147
+ expect(tables.length).toBe(1);
148
+ return {form, table: tables[0]};
149
+ }
150
+
151
+ function column(table, field)
152
+ {
153
+ return table.settings.columns.find((c) => c.field === field);
154
+ }
155
+
156
+ describe("a table field's validators", () => {
157
+ test("a template item's validator runs in its column, besides required", () => {
158
+ const fits = () => true;
159
+ const {table} = build(fits);
160
+ expect(column(table, "id").validator).toEqual(["required"]);
161
+ expect(column(table, "value").validator).toEqual([fits]);
162
+ });
163
+
164
+ test("a list of them, names and functions, is taken whole", () => {
165
+ const fits = () => true;
166
+ const {table} = build(["numeric", fits]);
167
+ expect(column(table, "value").validator).toEqual(["numeric", fits]);
168
+ });
169
+
170
+ test("an item without one keeps what the schema says", () => {
171
+ const {table} = build(null);
172
+ expect(column(table, "value").validator).toEqual([]);
173
+ });
174
+
175
+ test("a table with a bad cell refuses the save, and shows the cell", () => {
176
+ const {form, table} = build(null);
177
+ const $cell = document.createElement("div");
178
+ let shown = 0;
179
+ $cell.scrollIntoView = () => {
180
+ shown++;
181
+ };
182
+ table.answer = [{getElement: () => $cell}]; // what Tabulator answers
183
+ gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
184
+ expect(table.validated).toBe(1);
185
+ expect(saved).toEqual([]);
186
+ expect(shown).toBe(1);
187
+
188
+ table.answer = true;
189
+ gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
190
+ expect(saved.length).toBe(1);
191
+ });
192
+ });
@@ -0,0 +1,181 @@
1
+ /***********************************************************************
2
+ * form_template_field.wiring.test.js
3
+ *
4
+ * A `template` field of a C_YUI_FORM in 'exec' mode is a sub-form
5
+ * (a <fieldset>): a NODE of the record, whose value is the dict its
6
+ * own fields fill. From 7.26.8 to 7.26.32 the node went through the
7
+ * json conversion, which handed its null on: the fields below found
8
+ * no dict to write into (kw_set_dict_value() refuses a null segment)
9
+ * and the save stored the node as null -- the alarm colours of a
10
+ * device in estadodelaire, wiped by every save of its settings.
11
+ * Driven through the real gclass on a document double, with the
12
+ * template of those settings.
13
+ *
14
+ * Copyright (c) 2026, ArtGins.
15
+ * All Rights Reserved.
16
+ ***********************************************************************/
17
+ import {describe, test, expect, beforeAll, beforeEach} from "vitest";
18
+ import {install_dom_double} from "../test/dom_double.js";
19
+
20
+ install_dom_double();
21
+
22
+ if(typeof globalThis.ResizeObserver === "undefined") {
23
+ globalThis.ResizeObserver = class {
24
+ observe() {}
25
+ unobserve() {}
26
+ disconnect() {}
27
+ };
28
+ }
29
+
30
+ const {
31
+ SDATA_END, gclass_create,
32
+ gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
33
+ gobj_start, gobj_send_event, gobj_read_attr,
34
+ set_log_callback,
35
+ } = await import("@yuneta/gobj-js");
36
+ const {register_c_yui_form} = await import("./c_yui_form.js");
37
+
38
+ const saved = [];
39
+ const logged = [];
40
+ let yuno = null;
41
+
42
+ /* The settings template of an estadodelaire device type, cut down to
43
+ * the sub-form and one field beside it. */
44
+ const TEMPLATE = {
45
+ template_version: {
46
+ type: "integer",
47
+ default: 7,
48
+ flag: ["hidden", "persistent"]
49
+ },
50
+ alarm_colors: {
51
+ type: "object",
52
+ flag: ["writable", "persistent", "template"],
53
+ template: {
54
+ active_not_validated: {
55
+ type: "string",
56
+ default: "red",
57
+ flag: ["writable", "persistent", "color"]
58
+ },
59
+ active_validated: {
60
+ type: "string",
61
+ default: "#e5a50a",
62
+ flag: ["writable", "persistent", "color"]
63
+ },
64
+ not_active_not_validated: {
65
+ type: "string",
66
+ default: "Fuchsia",
67
+ flag: ["writable", "persistent", "color"]
68
+ }
69
+ }
70
+ }
71
+ };
72
+
73
+ beforeAll(async () => {
74
+ const i18next = (await import("i18next")).default;
75
+ await i18next.init({lng: "en", resources: {}});
76
+ gobj_start_up(null, null, null, null, null, null, null);
77
+ set_log_callback((level, msg) => {
78
+ logged.push({level: String(level), msg: String(msg)});
79
+ });
80
+ gclass_create("C_TEST_FORM_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
81
+ gclass_create(
82
+ "C_TEST_FORM_APP",
83
+ [["EV_SAVE_RECORD", 0], ["EV_RECORD_CHANGED", 0]],
84
+ [["ST_IDLE", [
85
+ ["EV_SAVE_RECORD", (gobj, event, kw) => {
86
+ saved.push(kw);
87
+ return 0;
88
+ }, null],
89
+ ["EV_RECORD_CHANGED", () => 0, null],
90
+ ]]],
91
+ {}, 0, [SDATA_END()], {}, 0, 0, 0, 0
92
+ );
93
+ register_c_yui_form();
94
+ yuno = gobj_create_yuno("form_template_yuno", "C_TEST_FORM_HOST", {});
95
+ gobj_start(yuno);
96
+ });
97
+
98
+ beforeEach(() => {
99
+ saved.length = 0;
100
+ logged.length = 0;
101
+ });
102
+
103
+ let seq = 0;
104
+
105
+ function build(record)
106
+ {
107
+ const app = gobj_create_pure_child(`tapp${++seq}`, "C_TEST_FORM_APP", {}, yuno);
108
+ gobj_start(app);
109
+ const form = gobj_create_pure_child(`tform${seq}`, "C_YUI_FORM", {
110
+ template: TEMPLATE,
111
+ record: record,
112
+ editable: true,
113
+ }, app);
114
+ gobj_start(form);
115
+ const $c = gobj_read_attr(form, "$container");
116
+ expect($c.querySelector("fieldset")).toBeTruthy(); // exec mode: a sub-form
117
+ return form;
118
+ }
119
+
120
+ function errors()
121
+ {
122
+ return logged.filter(l => /error/i.test(l.level) || /ERROR/.test(l.msg));
123
+ }
124
+
125
+ describe("a template field in exec mode (a sub-form)", () => {
126
+ test("is saved as the dict its fields fill", () => {
127
+ const form = build({
128
+ template_version: 7,
129
+ alarm_colors: {
130
+ active_not_validated: "#ff0000",
131
+ active_validated: "#e5a50a",
132
+ not_active_not_validated: "#ff00ff"
133
+ }
134
+ });
135
+ gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
136
+ expect(saved.length).toBe(1);
137
+ expect(saved[0].alarm_colors).toEqual({
138
+ active_not_validated: "#ff0000",
139
+ active_validated: "#e5a50a",
140
+ not_active_not_validated: "#ff00ff"
141
+ });
142
+ expect(saved[0].template_version).toBe(7);
143
+ expect(errors()).toEqual([]);
144
+ });
145
+
146
+ test("a colour cleared is black, and its readout says so", () => {
147
+ const form = build({
148
+ template_version: 7,
149
+ alarm_colors: {
150
+ active_not_validated: "#ff0000",
151
+ active_validated: "#e5a50a",
152
+ not_active_not_validated: "#ff00ff"
153
+ }
154
+ });
155
+ const $c = gobj_read_attr(form, "$container");
156
+ const $colors = $c.querySelectorAll("fieldset input");
157
+ expect($colors.length).toBe(3);
158
+ gobj_send_event(form, "EV_CLEAR_RECORD", {}, form);
159
+ for(const $input of $colors) {
160
+ /* Never "": a colour input refuses it (Chrome warns). */
161
+ expect($input.value).toBe("#000000");
162
+ expect($input.yui_color_value.textContent).toBe("#000000");
163
+ }
164
+ expect(errors()).toEqual([]);
165
+ });
166
+
167
+ test("left null by a save before the fix, comes back with its defaults", () => {
168
+ const form = build({
169
+ template_version: 7,
170
+ alarm_colors: null
171
+ });
172
+ gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
173
+ expect(saved.length).toBe(1);
174
+ expect(saved[0].alarm_colors).toEqual({
175
+ active_not_validated: "red",
176
+ active_validated: "#e5a50a",
177
+ not_active_not_validated: "Fuchsia"
178
+ });
179
+ expect(errors()).toEqual([]);
180
+ });
181
+ });