@yuneta/gobj-ui 7.25.8 → 7.25.10
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/README.md +72 -12
- package/dist/gobj-ui.cjs.js +327 -97
- package/dist/gobj-ui.es.js +327 -97
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +418 -76
- package/src/schema_editor_reload.wiring.test.js +551 -7
- package/src/shell_modals.js +17 -5
- package/src/shell_toast_once.test.js +77 -0
|
@@ -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,37 @@ 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 ones shown may be out of date, your next change reads them first";
|
|
304
|
+
const OWED = "the schemas shown may be out of date: they are read again, try again when they are in";
|
|
305
|
+
const NOT_SENT = "the treedb did not describe a write back: the writes after it were not sent";
|
|
306
|
+
|
|
267
307
|
describe("the screen of a reload", () => {
|
|
268
308
|
|
|
269
309
|
test("Refresh on the columns: the old screen goes, the toolbar stops answering", () => {
|
|
@@ -318,7 +358,7 @@ describe("the screen of a reload", () => {
|
|
|
318
358
|
|
|
319
359
|
describe("a form open when a reload starts", () => {
|
|
320
360
|
|
|
321
|
-
test("its Save is refused
|
|
361
|
+
test("its Save is refused and said, and the load that lands closes it: nothing written", () => {
|
|
322
362
|
const {editor, remote, host} = build("f1", "db/users");
|
|
323
363
|
gobj_send_event(editor, "EV_ADD_COLUMN", {}, editor);
|
|
324
364
|
expect(modals.length).toBe(1);
|
|
@@ -329,15 +369,17 @@ describe("a form open when a reload starts", () => {
|
|
|
329
369
|
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
330
370
|
|
|
331
371
|
expect(errors()).toEqual([]);
|
|
332
|
-
|
|
372
|
+
/* Not "try again": the same Save, once the load is in, wrote a
|
|
373
|
+
* form built on the model the load replaced. */
|
|
374
|
+
expect(shown).toEqual([WAIT]);
|
|
333
375
|
expect(form.closed).toBe(false);
|
|
334
376
|
expect(take("update-node")).toEqual([]);
|
|
335
377
|
|
|
336
378
|
answer_the_load(editor, remote);
|
|
337
|
-
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
338
379
|
expect(form.closed).toBe(true);
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
expect(shown).toEqual([WAIT, STALE]);
|
|
381
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
382
|
+
expect(take("update-node")).toEqual([]);
|
|
341
383
|
expect(errors()).toEqual([]);
|
|
342
384
|
});
|
|
343
385
|
|
|
@@ -346,7 +388,7 @@ describe("a form open when a reload starts", () => {
|
|
|
346
388
|
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
347
389
|
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id"}, editor);
|
|
348
390
|
expect(errors()).toEqual([]);
|
|
349
|
-
expect(shown).toEqual([
|
|
391
|
+
expect(shown).toEqual([WAIT]);
|
|
350
392
|
expect(take("update-node")).toEqual([]);
|
|
351
393
|
answer_the_load(editor, remote);
|
|
352
394
|
});
|
|
@@ -439,8 +481,10 @@ describe("a write given up that was DONE", () => {
|
|
|
439
481
|
expect(editor.priv.written).toEqual({"db.users": true});
|
|
440
482
|
published.length = 0;
|
|
441
483
|
|
|
484
|
+
/* The shape the store answers with: `nodes`/`update-node` ask
|
|
485
|
+
* for list_dict, so a fkey comes back as a list of refs. */
|
|
442
486
|
answer(editor, remote, w2, 0, {id: "db.users.email", value: "email",
|
|
443
|
-
type: "string", topics: ["
|
|
487
|
+
type: "string", topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
|
|
444
488
|
expect(published).toContain("EV_RECORD_WRITTEN");
|
|
445
489
|
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
446
490
|
answer_the_load(editor, remote);
|
|
@@ -471,3 +515,503 @@ describe("a write given up that was DONE", () => {
|
|
|
471
515
|
expect(editor.priv.written).toEqual({});
|
|
472
516
|
});
|
|
473
517
|
});
|
|
518
|
+
|
|
519
|
+
describe("a dialog built on the model a reload replaced (fourth review)", () => {
|
|
520
|
+
|
|
521
|
+
test("an edit form: the load that lands closes it, and its Save writes nothing", () => {
|
|
522
|
+
const {editor, remote, host} = build("s1", "db/users");
|
|
523
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
524
|
+
const form = modals[modals.length - 1];
|
|
525
|
+
form.$content.querySelector('[data-name="description"]').value = "mine";
|
|
526
|
+
|
|
527
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
528
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
529
|
+
r.cols[0].description = "NEWER";
|
|
530
|
+
r.cols[0].header = "Newer header";
|
|
531
|
+
}));
|
|
532
|
+
expect(form.closed).toBe(true);
|
|
533
|
+
expect(shown).toEqual([STALE]);
|
|
534
|
+
expect(editor.priv.dialog).toBe(null);
|
|
535
|
+
|
|
536
|
+
/* The form's button, pressed anyway (a stale screen, a keyboard):
|
|
537
|
+
* the record it would write is the OLD one with one field changed,
|
|
538
|
+
* over the newer one. */
|
|
539
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
540
|
+
expect(take("update-node")).toEqual([]);
|
|
541
|
+
expect(errors()).toEqual([]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("the column of the open form is gone after the reload: closed, no ERROR", () => {
|
|
545
|
+
const {editor, remote, host} = build("s2", "db/users");
|
|
546
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
547
|
+
const form = modals[modals.length - 1];
|
|
548
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
549
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
550
|
+
r.cols = [];
|
|
551
|
+
}));
|
|
552
|
+
expect(form.closed).toBe(true);
|
|
553
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
554
|
+
expect(take("update-node")).toEqual([]);
|
|
555
|
+
expect(errors()).toEqual([]);
|
|
556
|
+
expect(not_defined()).toEqual([]);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("an import plan is not run on the model that replaced the one it was planned on", () => {
|
|
560
|
+
const {editor, remote, host} = build("s3", "db");
|
|
561
|
+
gobj_send_event(editor, "EV_IMPORT", {}, host);
|
|
562
|
+
const dialog = modals[modals.length - 1];
|
|
563
|
+
gobj_send_event(editor, "EV_PREVIEW_IMPORT", {
|
|
564
|
+
prune: false,
|
|
565
|
+
text: JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
|
|
566
|
+
{id: "id", type: "string", flag: ["persistent", "required"]},
|
|
567
|
+
{id: "email", type: "string"}
|
|
568
|
+
]}]})
|
|
569
|
+
}, editor);
|
|
570
|
+
expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
|
|
571
|
+
|
|
572
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
573
|
+
expect(editor.priv.import_plan).toBe(null);
|
|
574
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
575
|
+
r.cols.push({id: "db.users.email", value: "email", order: 2, type: "integer",
|
|
576
|
+
topics: ["topics^db.users^cols"]});
|
|
577
|
+
}));
|
|
578
|
+
expect(dialog.closed).toBe(true);
|
|
579
|
+
|
|
580
|
+
dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled = false;
|
|
581
|
+
dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").click();
|
|
582
|
+
expect(take("update-node")).toEqual([]);
|
|
583
|
+
expect(take("delete-node")).toEqual([]);
|
|
584
|
+
expect(errors()).toEqual([]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("a confirmation decided on the model a reload replaced is not applied", () => {
|
|
588
|
+
const {editor, remote, host} = build("s4", "db/users");
|
|
589
|
+
const kw = {what: "column", topic: "users", col: "id",
|
|
590
|
+
model_gen: editor.priv.model_gen};
|
|
591
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
592
|
+
answer_the_load(editor, remote);
|
|
593
|
+
gobj_send_event(editor, "EV_CONFIRMED", kw, editor);
|
|
594
|
+
expect(take("update-node")).toEqual([]);
|
|
595
|
+
expect(shown).toEqual([STALE]);
|
|
596
|
+
expect(errors()).toEqual([]);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe("a load that fails IN session (fourth review)", () => {
|
|
601
|
+
|
|
602
|
+
test("keeps the model, the screen and the open form, says so, and owes the reload", () => {
|
|
603
|
+
const {editor, remote, host} = build("b1", "db/users");
|
|
604
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
605
|
+
const form = modals[modals.length - 1];
|
|
606
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
607
|
+
const asked = take("nodes");
|
|
608
|
+
answer(editor, remote, asked[0], -1, null, "deadline");
|
|
609
|
+
answer(editor, remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
|
|
610
|
+
answer(editor, remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
|
|
611
|
+
|
|
612
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
613
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
614
|
+
expect(editor.priv.records.treedbs.length).toBe(1);
|
|
615
|
+
expect(editor.priv.reload_on_open).toBe(true);
|
|
616
|
+
expect(shown).toEqual([KEPT]);
|
|
617
|
+
expect(form.closed).toBe(false);
|
|
618
|
+
expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBeTruthy();
|
|
619
|
+
|
|
620
|
+
/* The form was built on a model that may be older than the
|
|
621
|
+
* store: its Save would write every field of it. It runs the
|
|
622
|
+
* reload owed instead (fifth review), and the load that lands
|
|
623
|
+
* closes the form. */
|
|
624
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
625
|
+
expect(not_defined()).toEqual([]);
|
|
626
|
+
expect(take("update-node")).toEqual([]);
|
|
627
|
+
expect(shown).toEqual([KEPT, OWED]);
|
|
628
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
629
|
+
expect(form.closed).toBe(true);
|
|
630
|
+
expect(shown).toEqual([KEPT, OWED, STALE]);
|
|
631
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
632
|
+
expect(errors()).toEqual([]);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("with no model before it, it is the empty screen it always was", () => {
|
|
636
|
+
const remote = gobj_create_service("b2_remote", "C_TEST_REMOTE", {}, yuno);
|
|
637
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
638
|
+
const host = gobj_create("b2_host", "C_TEST_EDITOR_HOST", {}, yuno);
|
|
639
|
+
const editor = gobj_create("b2_editor", "C_YUI_SCHEMA_EDITOR", {
|
|
640
|
+
gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
|
|
641
|
+
}, host);
|
|
642
|
+
gobj_start(editor);
|
|
643
|
+
for(const c of take("nodes")) {
|
|
644
|
+
answer(editor, remote, c, -1, null, "deadline");
|
|
645
|
+
}
|
|
646
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
647
|
+
expect(editor.priv.model).toBe(null);
|
|
648
|
+
expect(errors()).toEqual([]);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("a drop that cuts a reload keeps the records the model was built on", () => {
|
|
652
|
+
const {editor, remote, host} = build("b3", "db/users");
|
|
653
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
654
|
+
const asked = take("nodes");
|
|
655
|
+
answer(editor, remote, asked[0], 0, []); /* half a load */
|
|
656
|
+
drop(editor, remote, host);
|
|
657
|
+
expect(editor.priv.records.treedbs.length).toBe(1);
|
|
658
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
659
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
describe("a late write, in the shape the store answers (fourth review)", () => {
|
|
664
|
+
|
|
665
|
+
test("a list_dict fkey marks the topic the column belongs to", () => {
|
|
666
|
+
const {editor, remote, host} = build("c1", "db/users");
|
|
667
|
+
const w = start_a_write(editor, host, "name");
|
|
668
|
+
drop(editor, remote, host);
|
|
669
|
+
reconnect(editor, remote, host);
|
|
670
|
+
answer_the_load(editor, remote);
|
|
671
|
+
expect(editor.priv.written).toEqual({});
|
|
672
|
+
answer(editor, remote, w, 0, {id: "db.users.name", value: "name", type: "string",
|
|
673
|
+
topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
|
|
674
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
675
|
+
answer_the_load(editor, remote);
|
|
676
|
+
expect(errors()).toEqual([]);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe("a write answered with no record (fourth review)", () => {
|
|
681
|
+
|
|
682
|
+
test("the reload it asks is the one owed: the next write does not load again", () => {
|
|
683
|
+
const {editor, remote, host} = build("n1", "db/users");
|
|
684
|
+
const w1 = start_a_write(editor, host, "a1");
|
|
685
|
+
drop(editor, remote, host);
|
|
686
|
+
reconnect(editor, remote, host);
|
|
687
|
+
answer_the_load(editor, remote);
|
|
688
|
+
const w2 = start_a_write(editor, host, "a2");
|
|
689
|
+
answer(editor, remote, w1, 0, {id: "db.users.a1", value: "a1",
|
|
690
|
+
topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
|
|
691
|
+
expect(editor.priv.reload_after_write).toBe(true);
|
|
692
|
+
answer(editor, remote, w2, 0, null);
|
|
693
|
+
expect(editor.priv.reload_after_write).toBe(false);
|
|
694
|
+
answer_the_load(editor, remote);
|
|
695
|
+
|
|
696
|
+
const w3 = start_a_write(editor, host, "a3");
|
|
697
|
+
answer(editor, remote, w3, 0, {id: "db.users.a3", value: "a3",
|
|
698
|
+
topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
|
|
699
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
700
|
+
expect(take("nodes")).toEqual([]);
|
|
701
|
+
expect(errors()).toEqual([]);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("the writes queued after it are said, not dropped in silence", () => {
|
|
705
|
+
const {editor, remote, host} = build("n2", "db/users");
|
|
706
|
+
for(const name of ["a", "b"]) {
|
|
707
|
+
const w = start_a_write(editor, host, name);
|
|
708
|
+
answer(editor, remote, w, 0, {id: `db.users.${name}`, value: name, type: "string",
|
|
709
|
+
order: name === "a" ? 2 : 3, topics: ["topics^db.users^cols"]});
|
|
710
|
+
}
|
|
711
|
+
gobj_send_event(editor, "EV_MOVE_COLUMN", {from: 0, to: 3}, host);
|
|
712
|
+
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
713
|
+
expect(editor.priv.save_queue.length).toBeGreaterThan(1);
|
|
714
|
+
const [first] = take("update-node");
|
|
715
|
+
answer(editor, remote, first, 0, null);
|
|
716
|
+
expect(take("update-node")).toEqual([]);
|
|
717
|
+
expect(shown).toEqual([NOT_SENT]);
|
|
718
|
+
expect(logged.some((l) => l.level === "warning" && /not sent/.test(l.msg))).toBe(true);
|
|
719
|
+
answer_the_load(editor, remote);
|
|
720
|
+
expect(errors()).toEqual([]);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
describe("the toolbar of a treedb that is not there (fourth review)", () => {
|
|
725
|
+
|
|
726
|
+
test("offers Back and Refresh, and nothing that needs the treedb", () => {
|
|
727
|
+
const {editor, remote, host} = build("t1", "db");
|
|
728
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
729
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
730
|
+
r.treedbs = [{id: "other", schema_version: 1}];
|
|
731
|
+
r.topics = [];
|
|
732
|
+
r.cols = [];
|
|
733
|
+
}));
|
|
734
|
+
expect($in(editor, ".SCHEMA_BACK")).toBeTruthy();
|
|
735
|
+
expect($in(editor, ".SCHEMA_REFRESH_BTN")).toBeTruthy();
|
|
736
|
+
for(const cls of ["SCHEMA_DIAGRAM_BTN", "SCHEMA_VALIDATE_BTN", "SCHEMA_EXPORT_BTN",
|
|
737
|
+
"SCHEMA_IMPORT_BTN", "SCHEMA_ADD_TOPIC_BTN"]) {
|
|
738
|
+
expect([cls, !!$in(editor, `.${cls}`)]).toEqual([cls, false]);
|
|
739
|
+
}
|
|
740
|
+
for(const $b of $all(editor, ".SCHEMA_TOOLBAR button")) {
|
|
741
|
+
if($b.classList.contains("SCHEMA_REFRESH_BTN") || $b.classList.contains("SCHEMA_BACK")) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
$b.click();
|
|
745
|
+
}
|
|
746
|
+
expect(errors()).toEqual([]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("a topic that is not there offers no New column", () => {
|
|
750
|
+
const {editor, remote, host} = build("t2", "db/users");
|
|
751
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
752
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
753
|
+
r.topics = [];
|
|
754
|
+
r.cols = [];
|
|
755
|
+
}));
|
|
756
|
+
expect(!!$in(editor, ".SCHEMA_ADD_COL_BTN")).toBe(false);
|
|
757
|
+
expect($in(editor, ".SCHEMA_BACK")).toBeTruthy();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe("every control of the editor's dialogs is named (fourth review)", () => {
|
|
762
|
+
|
|
763
|
+
/* A control a wrapping <label> names is the one shape that needs no
|
|
764
|
+
* attribute (CLAUDE.md, "title + aria-label"). */
|
|
765
|
+
function unnamed($root)
|
|
766
|
+
{
|
|
767
|
+
const out = [];
|
|
768
|
+
for(const $c of $root.querySelectorAll("button, input, select, textarea")) {
|
|
769
|
+
if($c.getAttribute("type") === "checkbox" && $c.closest("label")) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const ok = $c.getAttribute("title") !== null &&
|
|
773
|
+
$c.getAttribute("aria-label") !== null &&
|
|
774
|
+
!!$c.getAttribute("data-i18n-title") &&
|
|
775
|
+
!!$c.getAttribute("data-i18n-aria-label");
|
|
776
|
+
if(!ok) {
|
|
777
|
+
out.push(`${$c.tagName} ${$c.className || $c.getAttribute("data-name") || ""}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
test("the column form, the topic form and the import", () => {
|
|
784
|
+
const {editor, remote, host} = build("a1", "db/users");
|
|
785
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
786
|
+
expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
|
|
787
|
+
|
|
788
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
789
|
+
gobj_send_event(editor, "EV_EDIT_TOPIC", {topic: "users"}, host);
|
|
790
|
+
expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
|
|
791
|
+
|
|
792
|
+
gobj_send_event(editor, "EV_IMPORT", {}, host);
|
|
793
|
+
expect(unnamed(modals[modals.length - 1].$content)).toEqual([]);
|
|
794
|
+
expect(errors()).toEqual([]);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
describe("the export's two views are controls (fifth review)", () => {
|
|
799
|
+
|
|
800
|
+
test("each is a named button that says which one is shown, and switches the text", () => {
|
|
801
|
+
const {editor, remote, host} = build("x1", "db");
|
|
802
|
+
gobj_send_event(editor, "EV_EXPORT", {}, host);
|
|
803
|
+
const $content = modals[modals.length - 1].$content;
|
|
804
|
+
const $tabs = [...$content.querySelectorAll(".SCHEMA_EXPORT_TAB")];
|
|
805
|
+
expect($tabs.map(($b) => [
|
|
806
|
+
$b.tagName.toLowerCase(),
|
|
807
|
+
$b.getAttribute("type"),
|
|
808
|
+
$b.getAttribute("title") !== null,
|
|
809
|
+
$b.getAttribute("data-i18n-title"),
|
|
810
|
+
$b.getAttribute("aria-label") !== null,
|
|
811
|
+
$b.getAttribute("data-i18n-aria-label"),
|
|
812
|
+
$b.getAttribute("aria-pressed"),
|
|
813
|
+
])).toEqual([
|
|
814
|
+
["button", "button", true, "schema as c source", true, "schema as c source", "true"],
|
|
815
|
+
["button", "button", true, "schema as json", true, "schema as json", "false"],
|
|
816
|
+
]);
|
|
817
|
+
|
|
818
|
+
const $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
|
|
819
|
+
$tabs[1].click();
|
|
820
|
+
expect($text.value.trim().startsWith("{")).toBe(true);
|
|
821
|
+
expect($tabs.map(($b) => $b.getAttribute("aria-pressed"))).toEqual(["false", "true"]);
|
|
822
|
+
$tabs[0].click();
|
|
823
|
+
expect($text.value.trim().startsWith("{")).toBe(false);
|
|
824
|
+
expect(errors()).toEqual([]);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe("a Refresh asked while a load is in flight (fifth review)", () => {
|
|
829
|
+
|
|
830
|
+
test("keeps the records of the model shown, not the half the first load got", () => {
|
|
831
|
+
const {editor, remote, host} = build("h1", "db/users");
|
|
832
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
833
|
+
const first = take("nodes");
|
|
834
|
+
answer(editor, remote, first[0], 0, []); /* half a load */
|
|
835
|
+
|
|
836
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
837
|
+
const second = take("nodes");
|
|
838
|
+
expect(second.length).toBe(3);
|
|
839
|
+
drop(editor, remote, host);
|
|
840
|
+
expect(editor.priv.records.treedbs.length).toBe(1);
|
|
841
|
+
expect(editor.priv.records.topics.length).toBe(1);
|
|
842
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
843
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
844
|
+
expect(errors()).toEqual([]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("...and so does one that fails in session", () => {
|
|
848
|
+
const {editor, remote, host} = build("h2", "db/users");
|
|
849
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
850
|
+
const first = take("nodes");
|
|
851
|
+
answer(editor, remote, first[0], 0, []);
|
|
852
|
+
|
|
853
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
854
|
+
const second = take("nodes");
|
|
855
|
+
answer(editor, remote, second[0], -1, null, "deadline");
|
|
856
|
+
answer(editor, remote, second[1], 0, []);
|
|
857
|
+
answer(editor, remote, second[2], 0, []);
|
|
858
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
859
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
860
|
+
expect(errors()).toEqual([]);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
describe("the reload a load refused in session owes (fifth review)", () => {
|
|
865
|
+
|
|
866
|
+
function refused_load(name, subpath)
|
|
867
|
+
{
|
|
868
|
+
const built = build(name, subpath);
|
|
869
|
+
gobj_send_event(built.editor, "EV_REFRESH", {}, built.host);
|
|
870
|
+
const asked = take("nodes");
|
|
871
|
+
answer(built.editor, built.remote, asked[0], -1, null, "deadline");
|
|
872
|
+
answer(built.editor, built.remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
|
|
873
|
+
answer(built.editor, built.remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
|
|
874
|
+
expect(built.editor.priv.reload_on_open).toBe(true);
|
|
875
|
+
expect(shown).toEqual([KEPT]);
|
|
876
|
+
return built;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
test("RUNS on the next edit, which is refused and said: nothing is opened on the old model", () => {
|
|
880
|
+
const {editor, remote, host} = refused_load("o1", "db/users");
|
|
881
|
+
const before = modals.length;
|
|
882
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
883
|
+
expect(modals.length).toBe(before);
|
|
884
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
885
|
+
expect(shown).toEqual([KEPT, OWED]);
|
|
886
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
887
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
888
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
889
|
+
|
|
890
|
+
/* Once: the next edit opens its form. */
|
|
891
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
892
|
+
expect(modals.length).toBe(before + 1);
|
|
893
|
+
expect(take("nodes")).toEqual([]);
|
|
894
|
+
expect(errors()).toEqual([]);
|
|
895
|
+
expect(not_defined()).toEqual([]);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test.each([
|
|
899
|
+
["EV_ADD_COLUMN", {}],
|
|
900
|
+
["EV_DUPLICATE_COLUMN", {col: "id"}],
|
|
901
|
+
["EV_DELETE_COLUMN", {col: "id"}],
|
|
902
|
+
["EV_MOVE_COLUMN", {from: 0, to: 1}],
|
|
903
|
+
["EV_EXPORT", {}],
|
|
904
|
+
["EV_IMPORT", {}],
|
|
905
|
+
["EV_VALIDATE", {}],
|
|
906
|
+
["EV_CONFIRMED", {what: "column", topic: "users", col: "id"}],
|
|
907
|
+
])("%s runs it too, and writes nothing", (event, kw) => {
|
|
908
|
+
const {editor, remote, host} = refused_load(`o2_${event}`, "db/users");
|
|
909
|
+
kw = Object.assign({model_gen: editor.priv.model_gen}, kw);
|
|
910
|
+
const before = modals.length;
|
|
911
|
+
gobj_send_event(editor, event, kw, host);
|
|
912
|
+
expect(modals.length).toBe(before);
|
|
913
|
+
expect(take("update-node")).toEqual([]);
|
|
914
|
+
expect(take("delete-node")).toEqual([]);
|
|
915
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
916
|
+
expect(errors()).toEqual([]);
|
|
917
|
+
expect(not_defined()).toEqual([]);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("a move goes where it was going, and reads the schemas there", () => {
|
|
921
|
+
const {editor, remote, host} = refused_load("o3", "db/users");
|
|
922
|
+
gobj_send_event(editor, "EV_BACK", {}, host);
|
|
923
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
924
|
+
expect(shown).toEqual([KEPT]);
|
|
925
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
926
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
927
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
928
|
+
expect(errors()).toEqual([]);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("keeps what this session wrote", () => {
|
|
932
|
+
const {editor, remote, host} = build("o4", "db/users");
|
|
933
|
+
const w = start_a_write(editor, host, "name");
|
|
934
|
+
answer(editor, remote, w, 0, {id: "db.users.name", value: "name", order: 2,
|
|
935
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
936
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
937
|
+
editor.priv.reload_on_open = true; /* as a refused load leaves it */
|
|
938
|
+
gobj_send_event(editor, "EV_ADD_COLUMN", {}, host);
|
|
939
|
+
answer_the_load(editor, remote);
|
|
940
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe("the import plan (fifth review)", () => {
|
|
945
|
+
|
|
946
|
+
const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
|
|
947
|
+
{id: "id", type: "string", flag: ["persistent", "required"]},
|
|
948
|
+
{id: "email", type: "string"}
|
|
949
|
+
]}]});
|
|
950
|
+
|
|
951
|
+
test("a load that could not be sent keeps it: the model it was planned on stays", () => {
|
|
952
|
+
const {editor, remote, host} = build("i1", "db");
|
|
953
|
+
gobj_send_event(editor, "EV_IMPORT", {}, host);
|
|
954
|
+
const dialog = modals[modals.length - 1];
|
|
955
|
+
gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
|
|
956
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
957
|
+
const plan = editor.priv.import_plan;
|
|
958
|
+
expect(plan.writes.length).toBeGreaterThan(0);
|
|
959
|
+
|
|
960
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
961
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
962
|
+
expect(editor.priv.import_plan).toBe(plan);
|
|
963
|
+
expect(dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled).toBe(false);
|
|
964
|
+
expect(errors()).toEqual([]);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test("a Yes that finds it gone is a warning, and said -- not an ERROR", () => {
|
|
968
|
+
const {editor, remote, host} = build("i2", "db");
|
|
969
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "import",
|
|
970
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
971
|
+
expect(errors()).toEqual([]);
|
|
972
|
+
expect(logged.some((l) => l.level === "warning" && /import plan is gone/.test(l.msg))).toBe(true);
|
|
973
|
+
expect(shown).toEqual(["the import plan is gone: preview it again"]);
|
|
974
|
+
expect(take("update-node")).toEqual([]);
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
describe("a confirmation answered where no treedb is open (fifth review)", () => {
|
|
979
|
+
|
|
980
|
+
test("after a reload that landed on zero treedbs: refused as stale, not NOT DEFINED", () => {
|
|
981
|
+
const {editor, remote, host} = build("e1", "db/users");
|
|
982
|
+
const kw = {what: "column", topic: "users", col: "id", model_gen: editor.priv.model_gen};
|
|
983
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
984
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
985
|
+
r.treedbs = [];
|
|
986
|
+
r.topics = [];
|
|
987
|
+
r.cols = [];
|
|
988
|
+
}));
|
|
989
|
+
expect(gobj_current_state(editor)).toBe("ST_EMPTY");
|
|
990
|
+
gobj_send_event(editor, "EV_CONFIRMED", kw, editor);
|
|
991
|
+
expect(not_defined()).toEqual([]);
|
|
992
|
+
expect(errors()).toEqual([]);
|
|
993
|
+
expect(shown).toEqual([STALE]);
|
|
994
|
+
expect(take("delete-node")).toEqual([]);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
test("with no model at all (ST_IDLE): refused and said", () => {
|
|
998
|
+
const remote = gobj_create_service("e2_remote", "C_TEST_REMOTE", {}, yuno);
|
|
999
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
1000
|
+
const host = gobj_create("e2_host", "C_TEST_EDITOR_HOST", {}, yuno);
|
|
1001
|
+
const editor = gobj_create("e2_editor", "C_YUI_SCHEMA_EDITOR", {
|
|
1002
|
+
gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
|
|
1003
|
+
}, host);
|
|
1004
|
+
gobj_start(editor);
|
|
1005
|
+
for(const c of take("nodes")) {
|
|
1006
|
+
answer(editor, remote, c, -1, null, "deadline");
|
|
1007
|
+
}
|
|
1008
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
1009
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id",
|
|
1010
|
+
model_gen: editor.priv.model_gen - 1}, editor);
|
|
1011
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id",
|
|
1012
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
1013
|
+
expect(not_defined()).toEqual([]);
|
|
1014
|
+
expect(errors()).toEqual([]);
|
|
1015
|
+
expect(shown).toEqual([STALE, STALE]);
|
|
1016
|
+
});
|
|
1017
|
+
});
|
package/src/shell_modals.js
CHANGED
|
@@ -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",
|
|
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
|
]
|
|
@@ -277,14 +278,16 @@ export function yui_shell_show_modal(shell, content, opts)
|
|
|
277
278
|
let modal_children;
|
|
278
279
|
if(dialog) {
|
|
279
280
|
let header = ["div", {class: "MODAL_HEADER yui-dialog-header"}, [
|
|
280
|
-
["button", {class: "MODAL_BACK yui-dialog-back", type: "button",
|
|
281
|
+
["button", {class: "MODAL_BACK yui-dialog-back", type: "button", title: t("back"), "data-i18n-title": "back",
|
|
282
|
+
"aria-label": t("back"), "data-i18n-aria-label": "back"},
|
|
281
283
|
[["i", {class: "yi-arrow-left"}]]],
|
|
282
284
|
["span", {class: "MODAL_TITLE yui-dialog-title"},
|
|
283
285
|
(title_prefix ? [["span", {class: "MODAL_TITLE_PREFIX"}, title_prefix]] : [])
|
|
284
286
|
.concat(title
|
|
285
287
|
? [["span", {class: "MODAL_TITLE_KIND", i18n: title}, title]]
|
|
286
288
|
: [])],
|
|
287
|
-
["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button",
|
|
289
|
+
["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button", title: t("close"), "data-i18n-title": "close",
|
|
290
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"},
|
|
288
291
|
[["i", {class: "yi-xmark"}]]],
|
|
289
292
|
]];
|
|
290
293
|
let body = ["div", {class: "MODAL_BODY yui-dialog-body"}, inner ? [inner] : []];
|
|
@@ -304,7 +307,8 @@ export function yui_shell_show_modal(shell, content, opts)
|
|
|
304
307
|
if(with_close) {
|
|
305
308
|
modal_children.push(
|
|
306
309
|
["button", {class: "MODAL_CLOSE modal-close is-large",
|
|
307
|
-
|
|
310
|
+
title: t("close"), "data-i18n-title": "close",
|
|
311
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"}]
|
|
308
312
|
);
|
|
309
313
|
}
|
|
310
314
|
}
|
|
@@ -501,8 +505,15 @@ function build_dialog(shell, message, buttons, opts)
|
|
|
501
505
|
}
|
|
502
506
|
let btn_attrs = {class: cls, type: "button",
|
|
503
507
|
"data-modal-button-value": b.value};
|
|
508
|
+
/* Named by its own label key: its text is its name for the eye,
|
|
509
|
+
* and the title and aria-label say the same to the pointer and
|
|
510
|
+
* to a reader, re-translated with it. */
|
|
504
511
|
if(typeof b.label === "string") {
|
|
505
512
|
btn_attrs.i18n = b.label;
|
|
513
|
+
btn_attrs.title = t(b.label);
|
|
514
|
+
btn_attrs["data-i18n-title"] = b.label;
|
|
515
|
+
btn_attrs["aria-label"] = t(b.label);
|
|
516
|
+
btn_attrs["data-i18n-aria-label"] = b.label;
|
|
506
517
|
}
|
|
507
518
|
return ["button", btn_attrs, b.label];
|
|
508
519
|
});
|
|
@@ -519,7 +530,8 @@ function build_dialog(shell, message, buttons, opts)
|
|
|
519
530
|
);
|
|
520
531
|
}
|
|
521
532
|
$card_children.push(
|
|
522
|
-
["button", {class: "CONFIRM_CLOSE delete yui-confirm-x",
|
|
533
|
+
["button", {class: "CONFIRM_CLOSE delete yui-confirm-x", title: t("close"), "data-i18n-title": "close",
|
|
534
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"}],
|
|
523
535
|
["section", {class: "CONFIRM_BODY modal-card-body has-text-centered"},
|
|
524
536
|
$body_children],
|
|
525
537
|
["footer", {class: "CONFIRM_FOOT modal-card-foot"}, $footer_children]
|