@yuneta/gobj-ui 7.25.8 → 7.25.9

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.
@@ -16,6 +16,15 @@
16
16
  * the reload of a late write that forgot this session's marks and
17
17
  * never told the host a write had landed.
18
18
  *
19
+ * The fourth independent review (same day) found what a reload
20
+ * still left behind: a dialog opened on the model it replaced
21
+ * (its Save wrote the old record over the newer one), a load
22
+ * refused IN session that blanked the model, a late write whose
23
+ * list_dict fkey marked nothing, a write answered with no record
24
+ * that owed a second reload and dropped the rest of its queue in
25
+ * silence, and a toolbar gated on the state instead of on the
26
+ * treedb being there.
27
+ *
19
28
  * Driven through the FSM on a document double, with a fake
20
29
  * transport whose state is the one the library reads.
21
30
  *
@@ -264,6 +273,36 @@ function errors()
264
273
  return logged.filter((l) => l.level === "error").map((l) => l.msg);
265
274
  }
266
275
 
276
+ function not_defined()
277
+ {
278
+ return logged.filter((l) => /NOT DEFINED/i.test(l.msg)).map((l) => l.msg);
279
+ }
280
+
281
+ /* The load, answered with records other than the ones it started on:
282
+ * somebody else wrote the store meanwhile. */
283
+ function answer_the_load_with(editor, remote, records)
284
+ {
285
+ const asked = take("nodes");
286
+ for(const c of asked) {
287
+ answer(editor, remote, c, 0, JSON.parse(JSON.stringify(records[c.kw.topic_name])));
288
+ }
289
+ return asked.length;
290
+ }
291
+
292
+ function records_with(change)
293
+ {
294
+ const records = JSON.parse(JSON.stringify(RECORDS));
295
+ change(records);
296
+ return records;
297
+ }
298
+
299
+ /* What the editor says while a load is in the air, and when one lands
300
+ * under an open dialog. */
301
+ const WAIT = "the schemas are loading: wait for them";
302
+ const STALE = "the schemas were read again: open the dialog again";
303
+ const KEPT = "cannot read the schemas again: the previous ones stay";
304
+ const NOT_SENT = "the treedb did not describe a write back: the writes after it were not sent";
305
+
267
306
  describe("the screen of a reload", () => {
268
307
 
269
308
  test("Refresh on the columns: the old screen goes, the toolbar stops answering", () => {
@@ -318,7 +357,7 @@ describe("the screen of a reload", () => {
318
357
 
319
358
  describe("a form open when a reload starts", () => {
320
359
 
321
- test("its Save is refused, said, the form stays, and it saves once the load is in", () => {
360
+ test("its Save is refused and said, and the load that lands closes it: nothing written", () => {
322
361
  const {editor, remote, host} = build("f1", "db/users");
323
362
  gobj_send_event(editor, "EV_ADD_COLUMN", {}, editor);
324
363
  expect(modals.length).toBe(1);
@@ -329,15 +368,17 @@ describe("a form open when a reload starts", () => {
329
368
  form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
330
369
 
331
370
  expect(errors()).toEqual([]);
332
- expect(shown).toEqual(["the schemas are loading: try again when they are in"]);
371
+ /* Not "try again": the same Save, once the load is in, wrote a
372
+ * form built on the model the load replaced. */
373
+ expect(shown).toEqual([WAIT]);
333
374
  expect(form.closed).toBe(false);
334
375
  expect(take("update-node")).toEqual([]);
335
376
 
336
377
  answer_the_load(editor, remote);
337
- form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
338
378
  expect(form.closed).toBe(true);
339
- const [write] = take("update-node");
340
- expect(write.kw.record.value).toBe("email");
379
+ expect(shown).toEqual([WAIT, STALE]);
380
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
381
+ expect(take("update-node")).toEqual([]);
341
382
  expect(errors()).toEqual([]);
342
383
  });
343
384
 
@@ -346,7 +387,7 @@ describe("a form open when a reload starts", () => {
346
387
  gobj_send_event(editor, "EV_REFRESH", {}, host);
347
388
  gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id"}, editor);
348
389
  expect(errors()).toEqual([]);
349
- expect(shown).toEqual(["the schemas are loading: try again when they are in"]);
390
+ expect(shown).toEqual([WAIT]);
350
391
  expect(take("update-node")).toEqual([]);
351
392
  answer_the_load(editor, remote);
352
393
  });
@@ -439,8 +480,10 @@ describe("a write given up that was DONE", () => {
439
480
  expect(editor.priv.written).toEqual({"db.users": true});
440
481
  published.length = 0;
441
482
 
483
+ /* The shape the store answers with: `nodes`/`update-node` ask
484
+ * for list_dict, so a fkey comes back as a list of refs. */
442
485
  answer(editor, remote, w2, 0, {id: "db.users.email", value: "email",
443
- type: "string", topics: ["topics^db.users^cols"]});
486
+ type: "string", topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
444
487
  expect(published).toContain("EV_RECORD_WRITTEN");
445
488
  expect(gobj_current_state(editor)).toBe("ST_LOADING");
446
489
  answer_the_load(editor, remote);
@@ -471,3 +514,274 @@ describe("a write given up that was DONE", () => {
471
514
  expect(editor.priv.written).toEqual({});
472
515
  });
473
516
  });
517
+
518
+ describe("a dialog built on the model a reload replaced (fourth review)", () => {
519
+
520
+ test("an edit form: the load that lands closes it, and its Save writes nothing", () => {
521
+ const {editor, remote, host} = build("s1", "db/users");
522
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
523
+ const form = modals[modals.length - 1];
524
+ form.$content.querySelector('[data-name="description"]').value = "mine";
525
+
526
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
527
+ answer_the_load_with(editor, remote, records_with((r) => {
528
+ r.cols[0].description = "NEWER";
529
+ r.cols[0].header = "Newer header";
530
+ }));
531
+ expect(form.closed).toBe(true);
532
+ expect(shown).toEqual([STALE]);
533
+ expect(editor.priv.dialog).toBe(null);
534
+
535
+ /* The form's button, pressed anyway (a stale screen, a keyboard):
536
+ * the record it would write is the OLD one with one field changed,
537
+ * over the newer one. */
538
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
539
+ expect(take("update-node")).toEqual([]);
540
+ expect(errors()).toEqual([]);
541
+ });
542
+
543
+ test("the column of the open form is gone after the reload: closed, no ERROR", () => {
544
+ const {editor, remote, host} = build("s2", "db/users");
545
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
546
+ const form = modals[modals.length - 1];
547
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
548
+ answer_the_load_with(editor, remote, records_with((r) => {
549
+ r.cols = [];
550
+ }));
551
+ expect(form.closed).toBe(true);
552
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
553
+ expect(take("update-node")).toEqual([]);
554
+ expect(errors()).toEqual([]);
555
+ expect(not_defined()).toEqual([]);
556
+ });
557
+
558
+ test("an import plan is not run on the model that replaced the one it was planned on", () => {
559
+ const {editor, remote, host} = build("s3", "db");
560
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
561
+ const dialog = modals[modals.length - 1];
562
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {
563
+ prune: false,
564
+ text: JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
565
+ {id: "id", type: "string", flag: ["persistent", "required"]},
566
+ {id: "email", type: "string"}
567
+ ]}]})
568
+ }, editor);
569
+ expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
570
+
571
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
572
+ expect(editor.priv.import_plan).toBe(null);
573
+ answer_the_load_with(editor, remote, records_with((r) => {
574
+ r.cols.push({id: "db.users.email", value: "email", order: 2, type: "integer",
575
+ topics: ["topics^db.users^cols"]});
576
+ }));
577
+ expect(dialog.closed).toBe(true);
578
+
579
+ dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled = false;
580
+ dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").click();
581
+ expect(take("update-node")).toEqual([]);
582
+ expect(take("delete-node")).toEqual([]);
583
+ expect(errors()).toEqual([]);
584
+ });
585
+
586
+ test("a confirmation decided on the model a reload replaced is not applied", () => {
587
+ const {editor, remote, host} = build("s4", "db/users");
588
+ const kw = {what: "column", topic: "users", col: "id",
589
+ model_gen: editor.priv.model_gen};
590
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
591
+ answer_the_load(editor, remote);
592
+ gobj_send_event(editor, "EV_CONFIRMED", kw, editor);
593
+ expect(take("update-node")).toEqual([]);
594
+ expect(shown).toEqual([STALE]);
595
+ expect(errors()).toEqual([]);
596
+ });
597
+ });
598
+
599
+ describe("a load that fails IN session (fourth review)", () => {
600
+
601
+ test("keeps the model, the screen and the open form, says so, and owes the reload", () => {
602
+ const {editor, remote, host} = build("b1", "db/users");
603
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
604
+ const form = modals[modals.length - 1];
605
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
606
+ const asked = take("nodes");
607
+ answer(editor, remote, asked[0], -1, null, "deadline");
608
+ answer(editor, remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
609
+ answer(editor, remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
610
+
611
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
612
+ expect(editor.priv.model.treedbs.length).toBe(1);
613
+ expect(editor.priv.records.treedbs.length).toBe(1);
614
+ expect(editor.priv.reload_on_open).toBe(true);
615
+ expect(shown).toEqual([KEPT]);
616
+ expect(form.closed).toBe(false);
617
+ expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBeTruthy();
618
+
619
+ /* The form was built on the model still shown: it works. */
620
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
621
+ expect(not_defined()).toEqual([]);
622
+ expect(take("update-node").length).toBe(1);
623
+ expect(errors()).toEqual([]);
624
+ });
625
+
626
+ test("with no model before it, it is the empty screen it always was", () => {
627
+ const remote = gobj_create_service("b2_remote", "C_TEST_REMOTE", {}, yuno);
628
+ gobj_change_state(remote, "ST_SESSION");
629
+ const host = gobj_create("b2_host", "C_TEST_EDITOR_HOST", {}, yuno);
630
+ const editor = gobj_create("b2_editor", "C_YUI_SCHEMA_EDITOR", {
631
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
632
+ }, host);
633
+ gobj_start(editor);
634
+ for(const c of take("nodes")) {
635
+ answer(editor, remote, c, -1, null, "deadline");
636
+ }
637
+ expect(gobj_current_state(editor)).toBe("ST_IDLE");
638
+ expect(editor.priv.model).toBe(null);
639
+ expect(errors()).toEqual([]);
640
+ });
641
+
642
+ test("a drop that cuts a reload keeps the records the model was built on", () => {
643
+ const {editor, remote, host} = build("b3", "db/users");
644
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
645
+ const asked = take("nodes");
646
+ answer(editor, remote, asked[0], 0, []); /* half a load */
647
+ drop(editor, remote, host);
648
+ expect(editor.priv.records.treedbs.length).toBe(1);
649
+ expect(editor.priv.records.cols.length).toBe(1);
650
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
651
+ });
652
+ });
653
+
654
+ describe("a late write, in the shape the store answers (fourth review)", () => {
655
+
656
+ test("a list_dict fkey marks the topic the column belongs to", () => {
657
+ const {editor, remote, host} = build("c1", "db/users");
658
+ const w = start_a_write(editor, host, "name");
659
+ drop(editor, remote, host);
660
+ reconnect(editor, remote, host);
661
+ answer_the_load(editor, remote);
662
+ expect(editor.priv.written).toEqual({});
663
+ answer(editor, remote, w, 0, {id: "db.users.name", value: "name", type: "string",
664
+ topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
665
+ expect(editor.priv.written).toEqual({"db.users": true});
666
+ answer_the_load(editor, remote);
667
+ expect(errors()).toEqual([]);
668
+ });
669
+ });
670
+
671
+ describe("a write answered with no record (fourth review)", () => {
672
+
673
+ test("the reload it asks is the one owed: the next write does not load again", () => {
674
+ const {editor, remote, host} = build("n1", "db/users");
675
+ const w1 = start_a_write(editor, host, "a1");
676
+ drop(editor, remote, host);
677
+ reconnect(editor, remote, host);
678
+ answer_the_load(editor, remote);
679
+ const w2 = start_a_write(editor, host, "a2");
680
+ answer(editor, remote, w1, 0, {id: "db.users.a1", value: "a1",
681
+ topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
682
+ expect(editor.priv.reload_after_write).toBe(true);
683
+ answer(editor, remote, w2, 0, null);
684
+ expect(editor.priv.reload_after_write).toBe(false);
685
+ answer_the_load(editor, remote);
686
+
687
+ const w3 = start_a_write(editor, host, "a3");
688
+ answer(editor, remote, w3, 0, {id: "db.users.a3", value: "a3",
689
+ topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
690
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
691
+ expect(take("nodes")).toEqual([]);
692
+ expect(errors()).toEqual([]);
693
+ });
694
+
695
+ test("the writes queued after it are said, not dropped in silence", () => {
696
+ const {editor, remote, host} = build("n2", "db/users");
697
+ for(const name of ["a", "b"]) {
698
+ const w = start_a_write(editor, host, name);
699
+ answer(editor, remote, w, 0, {id: `db.users.${name}`, value: name, type: "string",
700
+ order: name === "a" ? 2 : 3, topics: ["topics^db.users^cols"]});
701
+ }
702
+ gobj_send_event(editor, "EV_MOVE_COLUMN", {from: 0, to: 3}, host);
703
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
704
+ expect(editor.priv.save_queue.length).toBeGreaterThan(1);
705
+ const [first] = take("update-node");
706
+ answer(editor, remote, first, 0, null);
707
+ expect(take("update-node")).toEqual([]);
708
+ expect(shown).toEqual([NOT_SENT]);
709
+ expect(logged.some((l) => l.level === "warning" && /not sent/.test(l.msg))).toBe(true);
710
+ answer_the_load(editor, remote);
711
+ expect(errors()).toEqual([]);
712
+ });
713
+ });
714
+
715
+ describe("the toolbar of a treedb that is not there (fourth review)", () => {
716
+
717
+ test("offers Back and Refresh, and nothing that needs the treedb", () => {
718
+ const {editor, remote, host} = build("t1", "db");
719
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
720
+ answer_the_load_with(editor, remote, records_with((r) => {
721
+ r.treedbs = [{id: "other", schema_version: 1}];
722
+ r.topics = [];
723
+ r.cols = [];
724
+ }));
725
+ expect($in(editor, ".SCHEMA_BACK")).toBeTruthy();
726
+ expect($in(editor, ".SCHEMA_REFRESH_BTN")).toBeTruthy();
727
+ for(const cls of ["SCHEMA_DIAGRAM_BTN", "SCHEMA_VALIDATE_BTN", "SCHEMA_EXPORT_BTN",
728
+ "SCHEMA_IMPORT_BTN", "SCHEMA_ADD_TOPIC_BTN"]) {
729
+ expect([cls, !!$in(editor, `.${cls}`)]).toEqual([cls, false]);
730
+ }
731
+ for(const $b of $all(editor, ".SCHEMA_TOOLBAR button")) {
732
+ if($b.classList.contains("SCHEMA_REFRESH_BTN") || $b.classList.contains("SCHEMA_BACK")) {
733
+ continue;
734
+ }
735
+ $b.click();
736
+ }
737
+ expect(errors()).toEqual([]);
738
+ });
739
+
740
+ test("a topic that is not there offers no New column", () => {
741
+ const {editor, remote, host} = build("t2", "db/users");
742
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
743
+ answer_the_load_with(editor, remote, records_with((r) => {
744
+ r.topics = [];
745
+ r.cols = [];
746
+ }));
747
+ expect(!!$in(editor, ".SCHEMA_ADD_COL_BTN")).toBe(false);
748
+ expect($in(editor, ".SCHEMA_BACK")).toBeTruthy();
749
+ });
750
+ });
751
+
752
+ describe("every control of the editor's dialogs is named (fourth review)", () => {
753
+
754
+ /* A control a wrapping <label> names is the one shape that needs no
755
+ * attribute (CLAUDE.md, "title + aria-label"). */
756
+ function unnamed($root)
757
+ {
758
+ const out = [];
759
+ for(const $c of $root.querySelectorAll("button, input, select, textarea")) {
760
+ if($c.getAttribute("type") === "checkbox" && $c.closest("label")) {
761
+ continue;
762
+ }
763
+ const ok = $c.getAttribute("title") !== null &&
764
+ $c.getAttribute("aria-label") !== null &&
765
+ !!$c.getAttribute("data-i18n-title") &&
766
+ !!$c.getAttribute("data-i18n-aria-label");
767
+ if(!ok) {
768
+ out.push(`${$c.tagName} ${$c.className || $c.getAttribute("data-name") || ""}`);
769
+ }
770
+ }
771
+ return out;
772
+ }
773
+
774
+ test("the column form, the topic form and the import", () => {
775
+ const {editor, remote, host} = build("a1", "db/users");
776
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
777
+ expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
778
+
779
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
780
+ gobj_send_event(editor, "EV_EDIT_TOPIC", {topic: "users"}, host);
781
+ expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
782
+
783
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
784
+ expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
785
+ expect(errors()).toEqual([]);
786
+ });
787
+ });
@@ -153,7 +153,8 @@ function show_notification(shell, kind, message, opts)
153
153
  ["div", {class: `TOAST notification yui-notification ${yui_tint(kind)}`,
154
154
  role: kind === "danger" ? "alert" : "status"},
155
155
  [
156
- ["button", {class: "TOAST_CLOSE delete", "aria-label": t("close"), "data-i18n-aria-label": "close"}],
156
+ ["button", {class: "TOAST_CLOSE delete", title: t("close"), "data-i18n-title": "close",
157
+ "aria-label": t("close"), "data-i18n-aria-label": "close"}],
157
158
  ["p", {...p_attrs, class: "TOAST_MSG"}, message]
158
159
  ]
159
160
  ]
@@ -284,7 +285,8 @@ export function yui_shell_show_modal(shell, content, opts)
284
285
  .concat(title
285
286
  ? [["span", {class: "MODAL_TITLE_KIND", i18n: title}, title]]
286
287
  : [])],
287
- ["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button", "aria-label": t("close"), "data-i18n-aria-label": "close"},
288
+ ["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button", title: t("close"), "data-i18n-title": "close",
289
+ "aria-label": t("close"), "data-i18n-aria-label": "close"},
288
290
  [["i", {class: "yi-xmark"}]]],
289
291
  ]];
290
292
  let body = ["div", {class: "MODAL_BODY yui-dialog-body"}, inner ? [inner] : []];
@@ -304,7 +306,8 @@ export function yui_shell_show_modal(shell, content, opts)
304
306
  if(with_close) {
305
307
  modal_children.push(
306
308
  ["button", {class: "MODAL_CLOSE modal-close is-large",
307
- "aria-label": t("close"), "data-i18n-aria-label": "close"}]
309
+ title: t("close"), "data-i18n-title": "close",
310
+ "aria-label": t("close"), "data-i18n-aria-label": "close"}]
308
311
  );
309
312
  }
310
313
  }
@@ -519,7 +522,8 @@ function build_dialog(shell, message, buttons, opts)
519
522
  );
520
523
  }
521
524
  $card_children.push(
522
- ["button", {class: "CONFIRM_CLOSE delete yui-confirm-x", "aria-label": t("close"), "data-i18n-aria-label": "close"}],
525
+ ["button", {class: "CONFIRM_CLOSE delete yui-confirm-x", title: t("close"), "data-i18n-title": "close",
526
+ "aria-label": t("close"), "data-i18n-aria-label": "close"}],
523
527
  ["section", {class: "CONFIRM_BODY modal-card-body has-text-centered"},
524
528
  $body_children],
525
529
  ["footer", {class: "CONFIRM_FOOT modal-card-foot"}, $footer_children]
@@ -11,6 +11,10 @@
11
11
  * review): its handle and its time are its own, and the toast
12
12
  * stays while any caller still holds it.
13
13
  *
14
+ * And every ✕ this module draws is a control with a NAME: a
15
+ * `title` and an `aria-label`, both translatable (fourth
16
+ * independent review: TOAST_CLOSE had the label only).
17
+ *
14
18
  * Copyright (c) 2026, ArtGins.
15
19
  * All Rights Reserved.
16
20
  ***********************************************************************/
@@ -22,6 +26,8 @@ install_dom_double();
22
26
  const {
23
27
  yui_shell_show_error,
24
28
  yui_shell_show_info,
29
+ yui_shell_show_modal,
30
+ yui_shell_confirm_danger,
25
31
  } = await import("./shell_modals.js");
26
32
 
27
33
  let shell = null;
@@ -123,3 +129,39 @@ describe("a repeat is its own caller", () => {
123
129
  expect(toasts().length).toBe(1);
124
130
  });
125
131
  });
132
+
133
+ describe("every close control is named", () => {
134
+
135
+ function named($b)
136
+ {
137
+ /* The text itself is whatever the app's i18next answers (none
138
+ * here); what is checked is that both are THERE and carry the
139
+ * key a language change re-translates them by. */
140
+ return {
141
+ title: $b.getAttribute("title") !== null,
142
+ title_key: $b.getAttribute("data-i18n-title"),
143
+ label: $b.getAttribute("aria-label") !== null,
144
+ label_key: $b.getAttribute("data-i18n-aria-label"),
145
+ };
146
+ }
147
+
148
+ const NAMED = {title: true, title_key: "close", label: true, label_key: "close"};
149
+
150
+ test("the toast's", () => {
151
+ yui_shell_show_error(shell, "the connection dropped");
152
+ expect(named(toasts()[0].querySelector(".TOAST_CLOSE"))).toEqual(NAMED);
153
+ });
154
+
155
+ test("a modal's, dialog or plain, and a confirmation's", () => {
156
+ shell.priv.layers.modal = document.createElement("div");
157
+ yui_shell_show_modal(shell, document.createElement("div"), {dialog: true, title: "x"});
158
+ yui_shell_show_modal(shell, document.createElement("div"), {});
159
+ yui_shell_confirm_danger(shell, "delete this column?");
160
+ const $closes = shell.priv.layers.modal.querySelectorAll(
161
+ ".MODAL_CLOSE, .CONFIRM_CLOSE");
162
+ expect($closes.length).toBe(3);
163
+ for(const $b of $closes) {
164
+ expect(named($b)).toEqual(NAMED);
165
+ }
166
+ });
167
+ });