@yuneta/gobj-ui 7.25.7 → 7.25.8
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 +32 -1
- package/dist/gobj-ui.cjs.js +204 -75
- package/dist/gobj-ui.es.js +204 -75
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +186 -50
- package/src/schema_editor_reload.wiring.test.js +473 -0
- package/src/shell_modals.js +50 -29
- package/src/shell_toast_once.test.js +64 -3
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_editor_reload.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_SCHEMA_EDITOR while it RELOADS (the third independent
|
|
5
|
+
* review, 2026-09-23). A reload entered ST_LOADING and drew
|
|
6
|
+
* nothing: the screen it replaced stayed up and clickable, and
|
|
7
|
+
* ST_LOADING declares none of its actions, so a click on a card,
|
|
8
|
+
* on Back, on the drawing -- or the Save of a column form left
|
|
9
|
+
* open -- answered "Event NOT DEFINED in state ST_LOADING" and
|
|
10
|
+
* the click, or the edit, was lost.
|
|
11
|
+
*
|
|
12
|
+
* And the lows of the same review: a position left over from a
|
|
13
|
+
* load that could not leave, a drop during a WRITE that dropped
|
|
14
|
+
* the host's position (a drop during a load applies it), the
|
|
15
|
+
* drawing of a treedb that is gone drawn over the old screen, and
|
|
16
|
+
* the reload of a late write that forgot this session's marks and
|
|
17
|
+
* never told the host a write had landed.
|
|
18
|
+
*
|
|
19
|
+
* Driven through the FSM on a document double, with a fake
|
|
20
|
+
* transport whose state is the one the library reads.
|
|
21
|
+
*
|
|
22
|
+
* Copyright (c) 2026, ArtGins.
|
|
23
|
+
* All Rights Reserved.
|
|
24
|
+
***********************************************************************/
|
|
25
|
+
import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
|
|
26
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
27
|
+
|
|
28
|
+
install_dom_double();
|
|
29
|
+
|
|
30
|
+
/* What the editor SAYS, and the dialogs it opens (their content is
|
|
31
|
+
* clicked like the operator would). */
|
|
32
|
+
const shown = [];
|
|
33
|
+
const modals = [];
|
|
34
|
+
vi.mock("./shell_modals.js", () => ({
|
|
35
|
+
yui_shell_show_error: (shell, message) => {
|
|
36
|
+
shown.push(message);
|
|
37
|
+
return {close() {}};
|
|
38
|
+
},
|
|
39
|
+
yui_shell_show_info: () => ({close() {}}),
|
|
40
|
+
yui_shell_show_modal: (shell, $content, opts) => {
|
|
41
|
+
const modal = {$content: $content, opts: opts, closed: false};
|
|
42
|
+
modals.push(modal);
|
|
43
|
+
return {close() {
|
|
44
|
+
modal.closed = true;
|
|
45
|
+
}};
|
|
46
|
+
},
|
|
47
|
+
yui_shell_confirm_danger: () => new Promise(() => {}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
SDATA, SDATA_END, data_type_t, event_flag_t,
|
|
52
|
+
gclass_create,
|
|
53
|
+
gobj_read_pointer_attr, gobj_write_attr, gobj_parent,
|
|
54
|
+
gobj_subscribe_event, gobj_publish_event,
|
|
55
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
56
|
+
gobj_start, gobj_send_event, gobj_read_attr,
|
|
57
|
+
gobj_change_state, gobj_current_state,
|
|
58
|
+
set_log_callback,
|
|
59
|
+
} = await import("@yuneta/gobj-js");
|
|
60
|
+
const {register_c_yui_schema_editor} = await import("./c_yui_schema_editor.js");
|
|
61
|
+
|
|
62
|
+
const logged = [];
|
|
63
|
+
const commands = [];
|
|
64
|
+
const published = [];
|
|
65
|
+
|
|
66
|
+
const RECORDS = {
|
|
67
|
+
treedbs: [{id: "db", schema_version: 4}],
|
|
68
|
+
topics: [
|
|
69
|
+
{id: "db.users", value: "users", order: 1, pkey: "id", topic_version: 2,
|
|
70
|
+
treedbs: ["treedbs^db^topics"]},
|
|
71
|
+
],
|
|
72
|
+
cols: [
|
|
73
|
+
{id: "db.users.id", value: "id", order: 1, type: "string",
|
|
74
|
+
flag: ["persistent", "required"], topics: ["topics^db.users^cols"]},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function remote_command_parser(gobj, command, kw, src)
|
|
79
|
+
{
|
|
80
|
+
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
81
|
+
return `${command}: not in session`;
|
|
82
|
+
}
|
|
83
|
+
commands.push({command, kw, src});
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let yuno = null;
|
|
88
|
+
|
|
89
|
+
beforeAll(() => {
|
|
90
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
91
|
+
set_log_callback((level, msg) => {
|
|
92
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
93
|
+
});
|
|
94
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
95
|
+
const heard = (gobj, event) => {
|
|
96
|
+
published.push(event);
|
|
97
|
+
return 0;
|
|
98
|
+
};
|
|
99
|
+
gclass_create(
|
|
100
|
+
"C_TEST_EDITOR_HOST",
|
|
101
|
+
[["EV_POSITION_CHANGED", 0], ["EV_RECORD_WRITTEN", 0], ["EV_SCHEMA_CHECKED", 0]],
|
|
102
|
+
[["ST_IDLE", [
|
|
103
|
+
["EV_POSITION_CHANGED", heard, null],
|
|
104
|
+
["EV_RECORD_WRITTEN", heard, null],
|
|
105
|
+
["EV_SCHEMA_CHECKED", heard, null]
|
|
106
|
+
]]],
|
|
107
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
108
|
+
);
|
|
109
|
+
gclass_create(
|
|
110
|
+
"C_TEST_REMOTE",
|
|
111
|
+
[],
|
|
112
|
+
[["ST_DISCONNECTED", []], ["ST_SESSION", []]],
|
|
113
|
+
{mt_command_parser: remote_command_parser},
|
|
114
|
+
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
115
|
+
);
|
|
116
|
+
/* The drawing, as the editor hosts it: a child that publishes a
|
|
117
|
+
* click on one of its nodes. The real one draws with G6. */
|
|
118
|
+
gclass_create(
|
|
119
|
+
"C_YUI_TREEDB_SCHEMA",
|
|
120
|
+
[["EV_SHOW", 0], ["EV_NODE_CLICK", event_flag_t.EVF_OUTPUT_EVENT]],
|
|
121
|
+
[["ST_IDLE", [["EV_SHOW", () => 0, null]]]],
|
|
122
|
+
{
|
|
123
|
+
mt_create: (gobj) => {
|
|
124
|
+
const $c = document.createElement("div");
|
|
125
|
+
const $node = document.createElement("button");
|
|
126
|
+
$node.className = "FAKE_DIAGRAM_NODE";
|
|
127
|
+
$node.addEventListener("click", () => {
|
|
128
|
+
gobj_publish_event(gobj, "EV_NODE_CLICK", {topic: "users"});
|
|
129
|
+
});
|
|
130
|
+
$c.appendChild($node);
|
|
131
|
+
gobj_write_attr(gobj, "$container", $c);
|
|
132
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
133
|
+
if(!subscriber) {
|
|
134
|
+
subscriber = gobj_parent(gobj);
|
|
135
|
+
}
|
|
136
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
0,
|
|
140
|
+
[
|
|
141
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, ""),
|
|
142
|
+
SDATA(data_type_t.DTP_JSON, "descs", 0, null, ""),
|
|
143
|
+
SDATA(data_type_t.DTP_STRING, "node_route", 0, "", ""),
|
|
144
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, ""),
|
|
145
|
+
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, ""),
|
|
146
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, ""),
|
|
147
|
+
SDATA_END()
|
|
148
|
+
],
|
|
149
|
+
{}, 0, 0, 0, 0
|
|
150
|
+
);
|
|
151
|
+
register_c_yui_schema_editor();
|
|
152
|
+
yuno = gobj_create_yuno("reload_yuno", "C_TEST_HOST", {});
|
|
153
|
+
gobj_start(yuno);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
logged.length = 0;
|
|
158
|
+
commands.length = 0;
|
|
159
|
+
shown.length = 0;
|
|
160
|
+
modals.length = 0;
|
|
161
|
+
published.length = 0;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function take(command)
|
|
165
|
+
{
|
|
166
|
+
const out = commands.filter((c) => c.command === command);
|
|
167
|
+
for(const c of out) {
|
|
168
|
+
commands.splice(commands.indexOf(c), 1);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function answer(editor, remote, request, result, data, comment)
|
|
174
|
+
{
|
|
175
|
+
gobj_send_event(editor, "EV_MT_COMMAND_ANSWER", {
|
|
176
|
+
result: result,
|
|
177
|
+
comment: comment || "",
|
|
178
|
+
data: data,
|
|
179
|
+
__md_iev__: {command_stack: [{command: request.command, kw: request.kw}]}
|
|
180
|
+
}, remote);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function answer_the_load(editor, remote)
|
|
184
|
+
{
|
|
185
|
+
const asked = take("nodes");
|
|
186
|
+
for(const c of asked) {
|
|
187
|
+
answer(editor, remote, c, 0, JSON.parse(JSON.stringify(RECORDS[c.kw.topic_name])));
|
|
188
|
+
}
|
|
189
|
+
return asked.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function drop(editor, remote, host)
|
|
193
|
+
{
|
|
194
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
195
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function reconnect(editor, remote, host)
|
|
199
|
+
{
|
|
200
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
201
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function build(name, subpath)
|
|
205
|
+
{
|
|
206
|
+
const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
|
|
207
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
208
|
+
const host = gobj_create(`${name}_host`, "C_TEST_EDITOR_HOST", {}, yuno);
|
|
209
|
+
const editor = gobj_create(`${name}_editor`, "C_YUI_SCHEMA_EDITOR", {
|
|
210
|
+
gobj_remote_yuno: remote,
|
|
211
|
+
treedb_name: "treedb_system_schema",
|
|
212
|
+
base_route: "/schemas"
|
|
213
|
+
}, host);
|
|
214
|
+
gobj_start(editor);
|
|
215
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
216
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
217
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: subpath}, host);
|
|
218
|
+
return {editor, remote, host};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function start_a_write(editor, host, name)
|
|
222
|
+
{
|
|
223
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
224
|
+
creating: true, topic: "users", values: {value: name || "name", type: "string"}
|
|
225
|
+
}, host);
|
|
226
|
+
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
227
|
+
const [write] = take("update-node");
|
|
228
|
+
expect(write).toBeTruthy();
|
|
229
|
+
return write;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function $in(editor, selector)
|
|
233
|
+
{
|
|
234
|
+
return gobj_read_attr(editor, "$container").querySelector(selector);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function $all(editor, selector)
|
|
238
|
+
{
|
|
239
|
+
return gobj_read_attr(editor, "$container").querySelectorAll(selector);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function is_disabled($b)
|
|
243
|
+
{
|
|
244
|
+
return $b.disabled === true || $b.getAttribute("disabled") != null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* What an operator can reach: every clickable thing of the view, pressed
|
|
248
|
+
* the way a browser presses it -- a disabled button does not click. */
|
|
249
|
+
function press_everything(editor)
|
|
250
|
+
{
|
|
251
|
+
let pressed = 0;
|
|
252
|
+
for(const $el of $all(editor, "button, [role=button]")) {
|
|
253
|
+
if(is_disabled($el)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
$el.click();
|
|
257
|
+
pressed++;
|
|
258
|
+
}
|
|
259
|
+
return pressed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function errors()
|
|
263
|
+
{
|
|
264
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
describe("the screen of a reload", () => {
|
|
268
|
+
|
|
269
|
+
test("Refresh on the columns: the old screen goes, the toolbar stops answering", () => {
|
|
270
|
+
const {editor, remote, host} = build("r1", "db/users");
|
|
271
|
+
expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBeTruthy();
|
|
272
|
+
|
|
273
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
274
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
275
|
+
expect($in(editor, ".SCHEMA_BODY").childNodes.length).toBe(0);
|
|
276
|
+
const $buttons = $all(editor, ".SCHEMA_TOOLBAR button");
|
|
277
|
+
expect($buttons.length).toBeGreaterThan(0);
|
|
278
|
+
for(const $b of $buttons) {
|
|
279
|
+
expect(is_disabled($b)).toBe(true);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
answer_the_load(editor, remote);
|
|
283
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
284
|
+
expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBeTruthy();
|
|
285
|
+
expect(errors()).toEqual([]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test.each([
|
|
289
|
+
["the treedbs", ""],
|
|
290
|
+
["the topics", "db"],
|
|
291
|
+
["the columns", "db/users"],
|
|
292
|
+
["the drawing", "db/diagram"],
|
|
293
|
+
])("everything clickable on %s during a reload: nothing NOT DEFINED", (label, subpath) => {
|
|
294
|
+
const {editor, remote, host} = build(`r2_${subpath.replace(/\W/g, "_")}`, subpath);
|
|
295
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
296
|
+
press_everything(editor);
|
|
297
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
298
|
+
expect(errors()).toEqual([]);
|
|
299
|
+
|
|
300
|
+
answer_the_load(editor, remote);
|
|
301
|
+
expect(errors()).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("the reload of a reconnect draws the same screen", () => {
|
|
305
|
+
const {editor, remote, host} = build("r3", "db/users");
|
|
306
|
+
start_a_write(editor, host);
|
|
307
|
+
drop(editor, remote, host);
|
|
308
|
+
reconnect(editor, remote, host);
|
|
309
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
310
|
+
expect($in(editor, ".SCHEMA_BODY").childNodes.length).toBe(0);
|
|
311
|
+
press_everything(editor);
|
|
312
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
313
|
+
answer_the_load(editor, remote);
|
|
314
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
315
|
+
expect(errors()).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("a form open when a reload starts", () => {
|
|
320
|
+
|
|
321
|
+
test("its Save is refused, said, the form stays, and it saves once the load is in", () => {
|
|
322
|
+
const {editor, remote, host} = build("f1", "db/users");
|
|
323
|
+
gobj_send_event(editor, "EV_ADD_COLUMN", {}, editor);
|
|
324
|
+
expect(modals.length).toBe(1);
|
|
325
|
+
const form = modals[0];
|
|
326
|
+
form.$content.querySelector('[data-name="value"]').value = "email";
|
|
327
|
+
|
|
328
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
329
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
330
|
+
|
|
331
|
+
expect(errors()).toEqual([]);
|
|
332
|
+
expect(shown).toEqual(["the schemas are loading: try again when they are in"]);
|
|
333
|
+
expect(form.closed).toBe(false);
|
|
334
|
+
expect(take("update-node")).toEqual([]);
|
|
335
|
+
|
|
336
|
+
answer_the_load(editor, remote);
|
|
337
|
+
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
338
|
+
expect(form.closed).toBe(true);
|
|
339
|
+
const [write] = take("update-node");
|
|
340
|
+
expect(write.kw.record.value).toBe("email");
|
|
341
|
+
expect(errors()).toEqual([]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a confirmation given during a reload is refused and said", () => {
|
|
345
|
+
const {editor, remote, host} = build("f2", "db/users");
|
|
346
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
347
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id"}, editor);
|
|
348
|
+
expect(errors()).toEqual([]);
|
|
349
|
+
expect(shown).toEqual(["the schemas are loading: try again when they are in"]);
|
|
350
|
+
expect(take("update-node")).toEqual([]);
|
|
351
|
+
answer_the_load(editor, remote);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("a position that waited", () => {
|
|
356
|
+
|
|
357
|
+
test("a load that could not leave does not keep it for a later load", () => {
|
|
358
|
+
const {editor, remote, host} = build("p1", "db/users");
|
|
359
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
360
|
+
take("nodes"); /* never answered */
|
|
361
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
362
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
363
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host); /* cannot leave */
|
|
364
|
+
|
|
365
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
366
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
367
|
+
|
|
368
|
+
reconnect(editor, remote, host);
|
|
369
|
+
answer_the_load(editor, remote);
|
|
370
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
371
|
+
answer_the_load(editor, remote);
|
|
372
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
373
|
+
expect(editor.priv.topic_name).toBe("users");
|
|
374
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("a load that could not leave keeps the records the model was built on", () => {
|
|
378
|
+
const {editor, remote, host} = build("p2", "db/users");
|
|
379
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
380
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
381
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
382
|
+
|
|
383
|
+
/* ...and the session back reads the model again. */
|
|
384
|
+
reconnect(editor, remote, host);
|
|
385
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
386
|
+
answer_the_load(editor, remote);
|
|
387
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("a Refresh out of session asks nothing and logs no error: the session back loads", () => {
|
|
391
|
+
const {editor, remote, host} = build("p4", "db/users");
|
|
392
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
393
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
394
|
+
expect(errors()).toEqual([]);
|
|
395
|
+
expect(shown).toEqual(["cannot reach the treedb"]);
|
|
396
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
397
|
+
reconnect(editor, remote, host);
|
|
398
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
399
|
+
expect(errors()).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("a drop during a WRITE applies it, as a drop during a load does", () => {
|
|
403
|
+
const {editor, remote, host} = build("p3", "db/users");
|
|
404
|
+
start_a_write(editor, host);
|
|
405
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
406
|
+
drop(editor, remote, host);
|
|
407
|
+
expect(shown).toEqual(["the connection dropped during the write"]);
|
|
408
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
409
|
+
expect(editor.priv.topic_name).toBe("");
|
|
410
|
+
expect(editor.priv.pending_seg).toBe(null);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("the drawing of a treedb that is not there", () => {
|
|
415
|
+
|
|
416
|
+
test("does not leave the old screen up", () => {
|
|
417
|
+
const {editor, remote, host} = build("d1", "db/users");
|
|
418
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "gone/diagram"}, host);
|
|
419
|
+
expect(gobj_current_state(editor)).toBe("ST_DIAGRAM");
|
|
420
|
+
expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBe(null);
|
|
421
|
+
expect($in(editor, ".SCHEMA_NOTICE_TEXT").getAttribute("data-i18n")).toBe("that treedb is not here any more");
|
|
422
|
+
expect(errors()).toEqual([]);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("a write given up that was DONE", () => {
|
|
427
|
+
|
|
428
|
+
test("the host is told, and the reload keeps what this session wrote", () => {
|
|
429
|
+
const {editor, remote, host} = build("l1", "db/users");
|
|
430
|
+
const w1 = start_a_write(editor, host, "name");
|
|
431
|
+
answer(editor, remote, w1, 0, {id: "db.users.name", value: "name", order: 2,
|
|
432
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
433
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
434
|
+
|
|
435
|
+
const w2 = start_a_write(editor, host, "email");
|
|
436
|
+
drop(editor, remote, host);
|
|
437
|
+
reconnect(editor, remote, host);
|
|
438
|
+
answer_the_load(editor, remote);
|
|
439
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
440
|
+
published.length = 0;
|
|
441
|
+
|
|
442
|
+
answer(editor, remote, w2, 0, {id: "db.users.email", value: "email",
|
|
443
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
444
|
+
expect(published).toContain("EV_RECORD_WRITTEN");
|
|
445
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
446
|
+
answer_the_load(editor, remote);
|
|
447
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
448
|
+
expect(errors()).toEqual([]);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("a write answered with no record: told, marked, and the body answers again", () => {
|
|
452
|
+
const {editor, remote, host} = build("l3", "db/users");
|
|
453
|
+
const w = start_a_write(editor, host, "name");
|
|
454
|
+
answer(editor, remote, w, 0, null);
|
|
455
|
+
expect(published).toContain("EV_RECORD_WRITTEN");
|
|
456
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
457
|
+
answer_the_load(editor, remote);
|
|
458
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
459
|
+
expect($in(editor, ".SCHEMA_BODY").classList.contains("SCHEMA_BUSY")).toBe(false);
|
|
460
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
461
|
+
expect(errors()).toEqual([]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("the host's Refresh still forgets them (a Save is what sends it)", () => {
|
|
465
|
+
const {editor, remote, host} = build("l2", "db/users");
|
|
466
|
+
const w1 = start_a_write(editor, host, "name");
|
|
467
|
+
answer(editor, remote, w1, 0, {id: "db.users.name", value: "name", order: 2,
|
|
468
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
469
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
470
|
+
answer_the_load(editor, remote);
|
|
471
|
+
expect(editor.priv.written).toEqual({});
|
|
472
|
+
});
|
|
473
|
+
});
|
package/src/shell_modals.js
CHANGED
|
@@ -113,12 +113,20 @@ function maybe_apply_translator($node, opts)
|
|
|
113
113
|
* Returns `{ close() }` so callers can dismiss programmatically.
|
|
114
114
|
*
|
|
115
115
|
* ONE toast per message on screen: a string message of the same
|
|
116
|
-
* kind as a toast still showing is not stacked under it
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
116
|
+
* kind as a toast still showing is not stacked under it. One
|
|
117
|
+
* close of a transport settles every request it cut, each view
|
|
118
|
+
* shows its failure, and the operator got a column of identical
|
|
119
|
+
* "the connection dropped". A message built of nodes is never
|
|
120
|
+
* merged: its text is not a key.
|
|
121
|
+
*
|
|
122
|
+
* A repeat is still a CALLER of its own: it gets its own handle
|
|
123
|
+
* and its own time, and the toast stays while any caller holds
|
|
124
|
+
* it -- until every handle is closed or has timed out. Handed the
|
|
125
|
+
* first caller's handle and timer, a repeat asking `timeout: 0`
|
|
126
|
+
* was dismissed on the first one's timer, and closing one
|
|
127
|
+
* caller's handle closed a toast another was still showing
|
|
128
|
+
* (third independent review). The operator's ✕ closes it for
|
|
129
|
+
* everybody.
|
|
122
130
|
***************************************************************/
|
|
123
131
|
function show_notification(shell, kind, message, opts)
|
|
124
132
|
{
|
|
@@ -128,12 +136,12 @@ function show_notification(shell, kind, message, opts)
|
|
|
128
136
|
return { close: () => {} };
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
let timeout = (opts && opts.timeout != null) ? opts.timeout : 5000;
|
|
131
140
|
let key = (typeof message === "string") ? `${kind}\u0000${message}` : null;
|
|
132
141
|
if(key !== null) {
|
|
133
142
|
for(let $shown of Array.from($layer.children)) {
|
|
134
143
|
if($shown.__toast_key__ === key && $shown.__toast__) {
|
|
135
|
-
$shown.__toast__.
|
|
136
|
-
return $shown.__toast__.handle;
|
|
144
|
+
return $shown.__toast__.hold(timeout);
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
}
|
|
@@ -153,44 +161,57 @@ function show_notification(shell, kind, message, opts)
|
|
|
153
161
|
$layer.appendChild($note);
|
|
154
162
|
maybe_apply_translator($note, opts);
|
|
155
163
|
|
|
156
|
-
let
|
|
164
|
+
let holders = new Set();
|
|
157
165
|
let closed = false;
|
|
158
166
|
|
|
159
|
-
let
|
|
167
|
+
let close_toast = function() {
|
|
160
168
|
if(closed) {
|
|
161
169
|
return;
|
|
162
170
|
}
|
|
163
171
|
closed = true;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
for(let holder of holders) {
|
|
173
|
+
if(holder.timeout_id) {
|
|
174
|
+
clearTimeout(holder.timeout_id);
|
|
175
|
+
holder.timeout_id = null;
|
|
176
|
+
}
|
|
167
177
|
}
|
|
178
|
+
holders.clear();
|
|
168
179
|
if($note.parentNode) {
|
|
169
180
|
$note.parentNode.removeChild($note);
|
|
170
181
|
}
|
|
171
182
|
};
|
|
172
183
|
|
|
184
|
+
/* One caller of this toast: its handle, and its time. */
|
|
185
|
+
let hold = function(ms) {
|
|
186
|
+
let holder = {timeout_id: null};
|
|
187
|
+
let release = function() {
|
|
188
|
+
if(!holders.has(holder)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if(holder.timeout_id) {
|
|
192
|
+
clearTimeout(holder.timeout_id);
|
|
193
|
+
holder.timeout_id = null;
|
|
194
|
+
}
|
|
195
|
+
holders.delete(holder);
|
|
196
|
+
if(holders.size === 0) {
|
|
197
|
+
close_toast();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
holders.add(holder);
|
|
201
|
+
if(ms > 0) {
|
|
202
|
+
holder.timeout_id = setTimeout(release, ms);
|
|
203
|
+
}
|
|
204
|
+
return { close: release };
|
|
205
|
+
};
|
|
206
|
+
|
|
173
207
|
let $del = $note.querySelector(".delete");
|
|
174
208
|
if($del) {
|
|
175
|
-
$del.addEventListener("click",
|
|
209
|
+
$del.addEventListener("click", close_toast);
|
|
176
210
|
}
|
|
177
211
|
|
|
178
|
-
let timeout = (opts && opts.timeout != null) ? opts.timeout : 5000;
|
|
179
|
-
let restart = function() {
|
|
180
|
-
if(timeout_id) {
|
|
181
|
-
clearTimeout(timeout_id);
|
|
182
|
-
timeout_id = null;
|
|
183
|
-
}
|
|
184
|
-
if(timeout > 0) {
|
|
185
|
-
timeout_id = setTimeout(close, timeout);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
restart();
|
|
189
|
-
|
|
190
|
-
let handle = { close };
|
|
191
212
|
$note.__toast_key__ = key;
|
|
192
|
-
$note.__toast__ = {
|
|
193
|
-
return
|
|
213
|
+
$note.__toast__ = {hold: hold};
|
|
214
|
+
return hold(timeout);
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* the operator got a column of identical "the connection
|
|
8
8
|
* dropped" (low of the independent review of the 2nd round).
|
|
9
9
|
*
|
|
10
|
+
* And a repeat is a caller of its own (third independent
|
|
11
|
+
* review): its handle and its time are its own, and the toast
|
|
12
|
+
* stays while any caller still holds it.
|
|
13
|
+
*
|
|
10
14
|
* Copyright (c) 2026, ArtGins.
|
|
11
15
|
* All Rights Reserved.
|
|
12
16
|
***********************************************************************/
|
|
13
|
-
import {describe, test, expect, beforeEach} from "vitest";
|
|
17
|
+
import {describe, test, expect, beforeEach, afterEach, vi} from "vitest";
|
|
14
18
|
import {install_dom_double} from "../test/dom_double.js";
|
|
15
19
|
|
|
16
20
|
install_dom_double();
|
|
@@ -55,10 +59,67 @@ describe("one toast per message on screen", () => {
|
|
|
55
59
|
expect(toasts().length).toBe(1);
|
|
56
60
|
});
|
|
57
61
|
|
|
58
|
-
test("the
|
|
59
|
-
yui_shell_show_error(shell, "the connection dropped");
|
|
62
|
+
test("the toast goes when EVERY handle of it is closed", () => {
|
|
63
|
+
const first = yui_shell_show_error(shell, "the connection dropped");
|
|
60
64
|
const again = yui_shell_show_error(shell, "the connection dropped");
|
|
61
65
|
again.close();
|
|
66
|
+
expect(toasts().length).toBe(1); /* the first still holds it */
|
|
67
|
+
first.close();
|
|
68
|
+
expect(toasts().length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* The third independent review: a repeat was handed the FIRST toast's
|
|
74
|
+
* handle and timer. A repeat asking `timeout: 0` was dismissed on the
|
|
75
|
+
* first one's timer, and closing one caller's handle closed the toast
|
|
76
|
+
* another caller was still showing.
|
|
77
|
+
*/
|
|
78
|
+
describe("a repeat is its own caller", () => {
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.useFakeTimers();
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a repeat asking timeout 0 is not dismissed on the first one's timer", () => {
|
|
88
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
89
|
+
const sticky = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
90
|
+
vi.advanceTimersByTime(5000);
|
|
91
|
+
expect(toasts().length).toBe(1);
|
|
92
|
+
sticky.close();
|
|
93
|
+
expect(toasts().length).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("closing the first caller's handle leaves the repeat's toast up", () => {
|
|
97
|
+
const first = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
98
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 3000});
|
|
99
|
+
first.close();
|
|
100
|
+
expect(toasts().length).toBe(1);
|
|
101
|
+
vi.advanceTimersByTime(3000);
|
|
102
|
+
expect(toasts().length).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the longest time wins, as a restart did", () => {
|
|
106
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
107
|
+
vi.advanceTimersByTime(800);
|
|
108
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
109
|
+
vi.advanceTimersByTime(500);
|
|
110
|
+
expect(toasts().length).toBe(1);
|
|
111
|
+
vi.advanceTimersByTime(500);
|
|
112
|
+
expect(toasts().length).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the operator's close takes it down for every caller", () => {
|
|
116
|
+
const first = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
117
|
+
const again = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
118
|
+
toasts()[0].querySelector(".TOAST_CLOSE").click();
|
|
62
119
|
expect(toasts().length).toBe(0);
|
|
120
|
+
first.close();
|
|
121
|
+
again.close();
|
|
122
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
123
|
+
expect(toasts().length).toBe(1);
|
|
63
124
|
});
|
|
64
125
|
});
|