@yuneta/gobj-ui 7.26.27 → 7.26.29

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.
Files changed (45) hide show
  1. package/README.md +590 -25
  2. package/dist/gobj-ui.cjs.js +1080 -229
  3. package/dist/gobj-ui.es.js +1081 -230
  4. package/package.json +4 -2
  5. package/scripts/validate-locales.mjs +2656 -315
  6. package/src/c_g6_nodes_tree.js +1086 -253
  7. package/src/c_yui_form.js +16 -4
  8. package/src/c_yui_fsm_graph.js +5 -0
  9. package/src/c_yui_gclass.js +11 -0
  10. package/src/c_yui_gobj_tree_js.js +18 -1
  11. package/src/c_yui_json_graph.js +5 -0
  12. package/src/c_yui_nav.js +21 -16
  13. package/src/c_yui_pager.js +1 -1
  14. package/src/c_yui_period.js +10 -3
  15. package/src/c_yui_schema_editor.js +34 -18
  16. package/src/c_yui_shell.js +11 -10
  17. package/src/c_yui_treedb_graph.js +17 -9
  18. package/src/c_yui_treedb_schema.js +5 -0
  19. package/src/c_yui_treedb_topic_with_form.js +9 -9
  20. package/src/c_yui_treedb_topics.js +13 -13
  21. package/src/c_yui_window.js +2 -2
  22. package/src/c_yui_window_manager.js +5 -2
  23. package/src/c_yui_wizard.js +8 -1
  24. package/src/form_json_field.js +7 -0
  25. package/src/g6_canvas_tab_order.js +84 -0
  26. package/src/g6_canvas_tab_order.wiring.test.js +232 -0
  27. package/src/g6_nodes_tree_controls.test.js +547 -0
  28. package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
  29. package/src/g6_nodes_tree_i18n_keys.test.js +241 -5
  30. package/src/i18n_key_functions.js +235 -0
  31. package/src/i18n_key_functions.test.js +1525 -0
  32. package/src/lib_graph.js +38 -1
  33. package/src/nav_backbar_name.wiring.test.js +82 -0
  34. package/src/schema_flags.js +47 -0
  35. package/src/schema_import.js +7 -0
  36. package/src/shell_modals.js +20 -16
  37. package/src/shell_route_map.js +13 -3
  38. package/src/wizard_helpers.js +8 -0
  39. package/src/yui_dev.js +16 -3
  40. package/src/yui_dev_controls.test.js +174 -0
  41. package/src/yui_gclass_view.js +1 -1
  42. package/src/yui_graph_camera.js +1 -1
  43. package/src/yui_install.js +10 -8
  44. package/src/yui_table_select.js +2 -2
  45. package/src/yui_time.js +17 -2
@@ -565,7 +565,7 @@ function show_load_error(gobj, message)
565
565
  $container.insertBefore($err, $container.firstChild);
566
566
  }
567
567
  let treedb = gobj_read_attr(gobj, "treedb_name") || "";
568
- $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
568
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || t("cannot load treedb"));
569
569
  }
570
570
 
571
571
  /************************************************************
@@ -1368,8 +1368,9 @@ function process_treedb_descs(gobj)
1368
1368
  );
1369
1369
 
1370
1370
  // TODO get icon from remote config
1371
- add_tab(gobj, gobj_topic_form, id, key, "yi-table");
1372
- add_topic_card(gobj, id, key, "yi-table", desc);
1371
+ /* `key` is a topic of the backend's schema: data */
1372
+ add_tab(gobj, gobj_topic_form, id, /* i18n: data */ key, "yi-table");
1373
+ add_topic_card(gobj, id, /* i18n: data */ key, "yi-table", desc);
1373
1374
 
1374
1375
  gobj_start(gobj_topic_form);
1375
1376
  }
@@ -1390,7 +1391,7 @@ function process_treedb_descs(gobj)
1390
1391
  if(gobj.priv._pending_info) {
1391
1392
  let ti = gobj.priv._pending_info;
1392
1393
  gobj.priv._pending_info = null;
1393
- show_topic_info(gobj, ti);
1394
+ show_topic_info(gobj, /* i18n: data */ ti); // a topic name
1394
1395
  } else if(href && href.indexOf("?") >= 0) {
1395
1396
  gobj_send_event(gobj, "EV_SHOW", {href: href}, gobj);
1396
1397
  } else {
@@ -2075,7 +2076,7 @@ function open_json_viewer(gobj, opts)
2075
2076
  close_json_viewer(gobj);
2076
2077
  return;
2077
2078
  }
2078
- priv.json_modal = yui_shell_show_modal(shell, $box, {
2079
+ priv.json_modal = yui_shell_show_modal(shell, /* i18n: data */ $box, {
2079
2080
  dialog: true,
2080
2081
  logical_class: "TREEDB_JSON_SHEET",
2081
2082
  title_prefix: priv.treedb_name,
@@ -2223,8 +2224,7 @@ function answer_print_tranger_refused(gobj, path, answer)
2223
2224
  gobj_send_event(jv, "EV_SUBTREE_ERROR",
2224
2225
  Object.assign({path: path}, answer), gobj);
2225
2226
  } else {
2226
- yui_shell_show_error(yui_shell_of(gobj),
2227
- answer.i18n || answer.error, {t: t});
2227
+ yui_shell_show_error(yui_shell_of(gobj), answer.i18n || answer.error, {t: t});
2228
2228
  }
2229
2229
  }
2230
2230
 
@@ -2274,7 +2274,7 @@ function request_print_tranger(gobj, path)
2274
2274
  }, gobj);
2275
2275
  if(ret) {
2276
2276
  log_error(ret);
2277
- answer_print_tranger_refused(gobj, path, {error: refusal_text(ret)});
2277
+ answer_print_tranger_refused(gobj, path, {error: /* i18n: data */ refusal_text(ret)});
2278
2278
  return;
2279
2279
  }
2280
2280
  if(path) {
@@ -2386,7 +2386,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2386
2386
  {path: path, error: comment || "print-tranger failed"}, gobj);
2387
2387
  } else {
2388
2388
  yui_shell_show_error(yui_shell_of(gobj),
2389
- comment || "print-tranger failed", {t: t});
2389
+ /* i18n: data */ comment || "print-tranger failed", {t: t});
2390
2390
  }
2391
2391
  return 0;
2392
2392
  }
@@ -2411,7 +2411,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2411
2411
  }
2412
2412
  if(result < 0) {
2413
2413
  yui_shell_show_error(yui_shell_of(gobj),
2414
- comment || "schema-file failed", {t: t});
2414
+ /* i18n: data */ comment || "schema-file failed", {t: t});
2415
2415
  return 0;
2416
2416
  }
2417
2417
  gobj_send_event(jv, "EV_SET_JSON", {json: data}, gobj);
@@ -2459,9 +2459,9 @@ function ac_mt_command_answer(gobj, event, kw, src)
2459
2459
  /* The schema couldn't load (not a treedb, no authz for it, backend
2460
2460
  * down…). Show an inline banner in the view rather than a blocking
2461
2461
  * app-modal that wedges the whole SPA behind an empty tab. */
2462
- show_load_error(gobj, t(comment));
2462
+ show_load_error(gobj, t(/* i18n: data */ comment));
2463
2463
  } else {
2464
- yui_shell_show_error(yui_shell_of(gobj), comment, {t: t});
2464
+ yui_shell_show_error(yui_shell_of(gobj), /* i18n: data */ comment, {t: t});
2465
2465
  /* A refused write leaves the TABLE showing what the operator
2466
2466
  * typed and the store holding what it had. That is tolerable
2467
2467
  * for a form (it stays open on the values it failed with) and
@@ -2737,7 +2737,7 @@ function ac_show(gobj, event, kw, src)
2737
2737
  ************************************************************/
2738
2738
  function ac_show_topic_info(gobj, event, kw, src)
2739
2739
  {
2740
- show_topic_info(gobj, kw && kw.topic);
2740
+ show_topic_info(gobj, /* i18n: data */ kw && kw.topic); // a topic name
2741
2741
  return 0;
2742
2742
  }
2743
2743
 
@@ -836,7 +836,7 @@ function close_window(gobj)
836
836
  return {state: "vetoed"};
837
837
  }
838
838
  const answer = yui_shell_confirm_yesnocancel(
839
- yui_shell_of(gobj), kw_close.warning,
839
+ yui_shell_of(gobj), /* i18n: data */ kw_close.warning,
840
840
  {t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
841
841
  ).then(function(answer) {
842
842
  if(answer !== "yes") {
@@ -909,7 +909,7 @@ function set_max_icon(gobj, maximized)
909
909
  }
910
910
  let $btn = $container.querySelector('.wc-max');
911
911
  if($btn) {
912
- let key = maximized ? 'restore' : 'maximize';
912
+ const key = maximized ? 'restore' : 'maximize';
913
913
  $btn.innerHTML = maximized ? WC_RESTORE : WC_MAX;
914
914
  $btn.setAttribute('title', t(key));
915
915
  $btn.setAttribute('data-i18n-title', key);
@@ -510,9 +510,12 @@ function ac_register_window(gobj, event, kw, src)
510
510
  label_items.push(['span', {class: 'yui-dock-label-prefix'},
511
511
  text_node(prefix)]); // data, not markup
512
512
  }
513
+ /* An FSM action: what it is given is the kw of EV_REGISTER_WINDOW,
514
+ * which no reader can follow. `title_key` is the window's `title`,
515
+ * a key its HOST chose when it created the window. */
513
516
  if(title_key) {
514
517
  label_items.push(
515
- ['span', {class: 'yui-dock-label-kind', i18n: title_key},
518
+ ['span', {class: 'yui-dock-label-kind', i18n: /* i18n: data */ title_key},
516
519
  kind_text || title_key]
517
520
  );
518
521
  }
@@ -532,7 +535,7 @@ function ac_register_window(gobj, event, kw, src)
532
535
  let $chip = createElement2(
533
536
  ['div', Object.assign(
534
537
  {class: 'yui-dock-chip', role: 'button', tabindex: '0', title: title},
535
- (!prefix && title_key) ? {'data-i18n-title': title_key} : {}
538
+ (!prefix && title_key) ? {'data-i18n-title': /* i18n: data */ title_key} : {}
536
539
  ), [
537
540
  chip_lead(kw.icon),
538
541
  ['span', {class: 'yui-dock-label'}, label_items],
@@ -75,6 +75,12 @@ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement roo
75
75
  SDATA_END()
76
76
  ];
77
77
 
78
+ /* The default keys of the three buttons, which reach t() through the
79
+ * attrs: published for a consumer's validate-locales, which asks the
80
+ * app's locales for every key of an exported `*_I18N_KEYS` array. A
81
+ * host that names its own buttons asks for its own keys besides. */
82
+ export const C_YUI_WIZARD_I18N_KEYS = ["confirm", "next", "back"];
83
+
78
84
  let PRIVATE_DATA = {
79
85
  steps: null,
80
86
  idx: 0,
@@ -267,7 +273,8 @@ function render_chrome(gobj)
267
273
 
268
274
  $back.classList.toggle('is-hidden', !model.show_back);
269
275
 
270
- $plabel.setAttribute('data-i18n', model.primary_label);
276
+ $plabel.setAttribute('data-i18n',
277
+ /* i18n: WIZARD_HELPERS_I18N_KEYS, C_YUI_WIZARD_I18N_KEYS */ model.primary_label);
271
278
  $plabel.textContent = t(model.primary_label);
272
279
  $primary.setAttribute('title', t(model.primary_label));
273
280
  $primary.setAttribute('data-i18n-title', model.primary_label);
@@ -64,6 +64,13 @@ function value_fits(value, shape)
64
64
  }
65
65
  }
66
66
 
67
+ /* The key parse_json_field() returns as data (its `error`), which the
68
+ * form hands to t(): published for a consumer's validate-locales, which
69
+ * asks the app's locales for every key of an exported `*_I18N_KEYS`
70
+ * array. i18n_key_functions.test.js holds it to every `error:` this
71
+ * module writes. */
72
+ export const FORM_JSON_FIELD_I18N_KEYS = ["invalid json"];
73
+
67
74
  /************************************************************
68
75
  * Parse what a json field holds.
69
76
  *
@@ -0,0 +1,84 @@
1
+ /***********************************************************************
2
+ * g6_canvas_tab_order.js
3
+ *
4
+ * The canvases of a G6 graph in the page's Tab order: the main
5
+ * one in its place, the other layers out of it.
6
+ *
7
+ * G6 stacks four canvases (background, main, label, transient)
8
+ * and gives each of them `tabIndex = 1` (configCanvasDom, when it
9
+ * builds the Canvas and again on a renderer change). A POSITIVE
10
+ * tabindex goes before every control of the page: Tab from a
11
+ * graph went to its other canvases, then to the top of the page
12
+ * and through every control there, and only then to the graph's
13
+ * own buttons. The main canvas -- the one G6 listens on, and the
14
+ * one a view puts the keyboard on -- takes 0, so Tab goes from it
15
+ * to the graph's toolbars and buttons in DOM order; the other
16
+ * layers receive nothing (`pointer-events: none`) and take -1.
17
+ *
18
+ * Every view that creates a G6 Graph calls
19
+ * keep_canvas_tab_order() right after `new Graph()`: the order is
20
+ * put right each time G6 writes the 1 -- once the canvas is built
21
+ * (on the first render(), whichever function of the view calls
22
+ * it), and after a renderer change. One place, so a view with
23
+ * several render() calls cannot miss the one that builds the
24
+ * canvas.
25
+ *
26
+ * Copyright (c) 2026, ArtGins.
27
+ * All Rights Reserved.
28
+ ***********************************************************************/
29
+ import {
30
+ gobj_short_name,
31
+ log_error,
32
+ } from "@yuneta/gobj-js";
33
+
34
+ import {GraphEvent} from "@antv/g6";
35
+
36
+ /************************************************************
37
+ * Main canvas 0, every other layer -1. Returns 0, or -1 with
38
+ * the cause logged.
39
+ ************************************************************/
40
+ function set_canvas_tab_order(gobj, graph)
41
+ {
42
+ let canvas = graph? graph.getCanvas() : null;
43
+
44
+ if(!canvas || typeof canvas.getLayers !== "function") {
45
+ log_error(`${gobj_short_name(gobj)}: no canvas to set the Tab order of`);
46
+ return -1;
47
+ }
48
+
49
+ let ret = 0;
50
+ let layers = canvas.getLayers() || {};
51
+ for(let name of Object.keys(layers)) {
52
+ let $el = null;
53
+ try {
54
+ $el = layers[name].getContextService().getDomElement();
55
+ } catch(e) {
56
+ $el = null;
57
+ }
58
+ if(!$el) {
59
+ log_error(`${gobj_short_name(gobj)}: canvas layer '${name}' has no element`);
60
+ ret = -1;
61
+ continue;
62
+ }
63
+ $el.tabIndex = (name === "main")? 0 : -1;
64
+ }
65
+ return ret;
66
+ }
67
+
68
+ /************************************************************
69
+ * Keep the order for the life of `graph`: on every moment G6
70
+ * writes its own.
71
+ ************************************************************/
72
+ function keep_canvas_tab_order(gobj, graph)
73
+ {
74
+ for(let ev of [GraphEvent.AFTER_CANVAS_INIT, GraphEvent.AFTER_RENDERER_CHANGE]) {
75
+ graph.on(ev, () => {
76
+ set_canvas_tab_order(gobj, graph);
77
+ });
78
+ }
79
+ }
80
+
81
+ export {
82
+ set_canvas_tab_order,
83
+ keep_canvas_tab_order,
84
+ };
@@ -0,0 +1,232 @@
1
+ /***********************************************************************
2
+ * g6_canvas_tab_order.wiring.test.js
3
+ *
4
+ * Every view that draws a G6 graph puts its canvases in the page's
5
+ * Tab order the same way (g6_canvas_tab_order.js): the main canvas
6
+ * 0, the other layers -1 (REV-CLOUD V20.5 #2, and the four views
7
+ * the second pass left out).
8
+ *
9
+ * G6 stacks four canvases and gives each `tabIndex = 1`; a
10
+ * positive tabindex goes before every control of the page, so Tab
11
+ * from a graph went through its other canvases and the whole page
12
+ * before it reached the graph's own buttons.
13
+ *
14
+ * The real gclasses, on a G6 Graph double that does what G6 does
15
+ * here: the FIRST render() builds the four canvases with
16
+ * `tabIndex = 1` and says so with `aftercanvasinit`; a renderer
17
+ * change writes the 1 again and says `afterrendererchange`.
18
+ *
19
+ * Copyright (c) 2026, ArtGins.
20
+ * All Rights Reserved.
21
+ ***********************************************************************/
22
+ import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
23
+ import {install_dom_double} from "../test/dom_double.js";
24
+
25
+ install_dom_double();
26
+
27
+ /* The views watch their container; nothing resizes here. */
28
+ if(typeof globalThis.ResizeObserver === "undefined") {
29
+ globalThis.ResizeObserver = class {
30
+ observe() {
31
+ }
32
+ unobserve() {
33
+ }
34
+ disconnect() {
35
+ }
36
+ };
37
+ }
38
+
39
+ const graphs = vi.hoisted(() => []);
40
+ vi.mock("@antv/g6", async (importOriginal) => {
41
+ const real = await importOriginal();
42
+ class FakeGraph {
43
+ constructor(options) {
44
+ this.options = options;
45
+ this.handlers = {};
46
+ this.layers = null;
47
+ graphs.push(this);
48
+ return new Proxy(this, {
49
+ get: (t, k) => (k in t) ? t[k] : (() => undefined),
50
+ });
51
+ }
52
+ container_el() {
53
+ const c = this.options.container;
54
+ return (typeof c === "string") ? document.getElementById(c) : c;
55
+ }
56
+ write_canvas_dom() {
57
+ for(const name of Object.keys(this.layers)) {
58
+ this.layers[name].el.tabIndex = 1;
59
+ }
60
+ }
61
+ render() {
62
+ if(!this.layers) {
63
+ const $c = this.container_el();
64
+ this.layers = {};
65
+ for(const name of ["background", "main", "label", "transient"]) {
66
+ const el = document.createElement("canvas");
67
+ el.setAttribute("data-layer", name);
68
+ $c.appendChild(el);
69
+ this.layers[name] = {
70
+ el: el,
71
+ getContextService: () => ({getDomElement: () => el}),
72
+ };
73
+ }
74
+ this.write_canvas_dom();
75
+ this.emit(real.GraphEvent.AFTER_CANVAS_INIT, {canvas: this.getCanvas()});
76
+ }
77
+ return Promise.resolve();
78
+ }
79
+ setRenderer() {
80
+ this.write_canvas_dom();
81
+ this.emit(real.GraphEvent.AFTER_RENDERER_CHANGE, {});
82
+ }
83
+ getCanvas() {
84
+ return {
85
+ getLayers: () => this.layers,
86
+ getContainer: () => this.container_el(),
87
+ };
88
+ }
89
+ on(name, fn) {
90
+ (this.handlers[name] = this.handlers[name] || []).push(fn);
91
+ }
92
+ emit(name, ev) {
93
+ for(const fn of (this.handlers[name] || [])) {
94
+ fn(ev);
95
+ }
96
+ }
97
+ getZoom() {
98
+ return 1;
99
+ }
100
+ getData() {
101
+ return {nodes: [], edges: [], combos: []};
102
+ }
103
+ getNodeData() {
104
+ return [];
105
+ }
106
+ getEdgeData() {
107
+ return [];
108
+ }
109
+ getPlugins() {
110
+ return [];
111
+ }
112
+ getBehaviors() {
113
+ return [];
114
+ }
115
+ getOptions() {
116
+ return {behaviors: []};
117
+ }
118
+ draw() {
119
+ return Promise.resolve();
120
+ }
121
+ fitView() {
122
+ return Promise.resolve();
123
+ }
124
+ zoomTo() {
125
+ return Promise.resolve();
126
+ }
127
+ destroy() {
128
+ }
129
+ }
130
+ return {...real, Graph: FakeGraph};
131
+ });
132
+
133
+ const {
134
+ SDATA_END, gclass_create, gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
135
+ gobj_start, gobj_read_attr, set_log_callback, register_c_timer,
136
+ } = await import("@yuneta/gobj-js");
137
+ const {register_c_yui_json_graph} = await import("./c_yui_json_graph.js");
138
+ const {register_c_yui_gobj_tree_js} = await import("./c_yui_gobj_tree_js.js");
139
+ const {register_c_yui_fsm_graph} = await import("./c_yui_fsm_graph.js");
140
+ const {register_c_yui_treedb_schema} = await import("./c_yui_treedb_schema.js");
141
+ const {register_c_g6_nodes_tree} = await import("./c_g6_nodes_tree.js");
142
+
143
+ const logged = [];
144
+ let yuno = null;
145
+
146
+ beforeAll(async () => {
147
+ const i18next = (await import("i18next")).default;
148
+ await i18next.init({lng: "en", resources: {en: {translation: {}}}});
149
+ gobj_start_up(null, null, null, null, null, null, null);
150
+ set_log_callback((level, msg) => {
151
+ logged.push({level: String(level), msg: String(msg)});
152
+ });
153
+ register_c_timer();
154
+ register_c_yui_json_graph();
155
+ register_c_yui_gobj_tree_js();
156
+ register_c_yui_fsm_graph();
157
+ register_c_yui_treedb_schema();
158
+ register_c_g6_nodes_tree();
159
+ gclass_create("C_TEST_TAB_ORDER_HOST", [], [["ST_IDLE", []]], {}, 0,
160
+ [SDATA_END()], {}, 0, 0, 0, 0);
161
+ yuno = gobj_create_yuno("tab_order_yuno", "C_TEST_TAB_ORDER_HOST", {});
162
+ });
163
+
164
+ beforeEach(() => {
165
+ logged.length = 0;
166
+ });
167
+
168
+ const DESC = {topic_name: "users", cols: [
169
+ {id: "id", type: "string", flag: ["persistent"]},
170
+ ]};
171
+
172
+ /* Each view, with what it needs to render a first time. */
173
+ const VIEWS = {
174
+ C_YUI_JSON_GRAPH: {json_data: {a: 1, b: [1, 2]}},
175
+ C_YUI_GOBJ_TREE_JS: {},
176
+ C_YUI_FSM_GRAPH: {fsm: {
177
+ entry_state: "ST_IDLE",
178
+ states: [{name: "ST_IDLE"}, {name: "ST_BUSY"}],
179
+ rows: [
180
+ {event: "EV_GO", cells: [{next_state: "ST_BUSY"}, null]},
181
+ {event: "EV_DONE", cells: [null, {next_state: "ST_IDLE"}]},
182
+ ],
183
+ published: [],
184
+ unreachable: [],
185
+ }},
186
+ C_YUI_TREEDB_SCHEMA: {descs: {users: DESC}},
187
+ C_G6_NODES_TREE: {with_toolbar: false, operation_mode: "reading"},
188
+ };
189
+
190
+ let seq = 0;
191
+
192
+ async function mount(gclass_name, attrs)
193
+ {
194
+ const $container = document.createElement("div");
195
+ document.body.appendChild($container);
196
+ const n0 = graphs.length;
197
+ const view = gobj_create_pure_child(`tab_order_${++seq}`, gclass_name,
198
+ Object.assign({$container: $container}, attrs), yuno);
199
+ /* Mounted as a host mounts it: a view that builds its own root
200
+ * (json, gobj tree) is in the document before it starts, and
201
+ * G6 finds its canvas mount there by id. */
202
+ document.body.appendChild(gobj_read_attr(view, "$container"));
203
+ gobj_start(view);
204
+ /* render() resolves on its own time, as G6's does. */
205
+ await new Promise((resolve) => setTimeout(resolve, 0));
206
+ return {view, $container, graph: graphs.length > n0 ? graphs[graphs.length - 1] : null};
207
+ }
208
+
209
+ function tab_order(graph)
210
+ {
211
+ const order = {};
212
+ for(const [name, layer] of Object.entries(graph.layers || {})) {
213
+ order[name] = layer.el.tabIndex;
214
+ }
215
+ return order;
216
+ }
217
+
218
+ describe("every G6 view: the main canvas in the Tab order, the other layers out", () => {
219
+ for(const [gclass_name, attrs] of Object.entries(VIEWS)) {
220
+ test(`${gclass_name}: after its first render, and after a renderer change`, async () => {
221
+ const {graph} = await mount(gclass_name, attrs);
222
+ expect(graph, `${gclass_name} built a graph`).toBeTruthy();
223
+ expect(graph.layers, `${gclass_name} rendered when it started`).toBeTruthy();
224
+ expect(tab_order(graph)).toEqual({background: -1, main: 0, label: -1, transient: -1});
225
+
226
+ graph.setRenderer();
227
+ expect(tab_order(graph)).toEqual({background: -1, main: 0, label: -1, transient: -1});
228
+
229
+ expect(logged.filter((l) => l.level === "error").map((l) => l.msg)).toEqual([]);
230
+ });
231
+ }
232
+ });