@yuneta/gobj-ui 7.25.6 → 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 +53 -1
- package/dist/gobj-ui.cjs.js +330 -63
- package/dist/gobj-ui.es.js +330 -63
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +339 -66
- package/src/schema_editor_reload.wiring.test.js +473 -0
- package/src/schema_editor_transport.wiring.test.js +174 -1
- package/src/shell_modals.js +61 -12
- package/src/shell_toast_once.test.js +125 -0
|
@@ -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
|
+
});
|
|
@@ -56,8 +56,13 @@ const RECORDS = {
|
|
|
56
56
|
],
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
/* A transport out of session refuses in the RETURN, as C_IEVENT_CLI and
|
|
60
|
+
* the routing adapters do: a request that did not leave is not waited for. */
|
|
59
61
|
function remote_command_parser(gobj, command, kw, src)
|
|
60
62
|
{
|
|
63
|
+
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
64
|
+
return `${command}: not in session`;
|
|
65
|
+
}
|
|
61
66
|
commands.push({command, kw, src});
|
|
62
67
|
return null;
|
|
63
68
|
}
|
|
@@ -123,7 +128,10 @@ function answer_the_load(editor, remote)
|
|
|
123
128
|
{
|
|
124
129
|
const asked = take("nodes");
|
|
125
130
|
for(const c of asked) {
|
|
126
|
-
|
|
131
|
+
/* A copy, as a transport hands it: the editor patches what it is
|
|
132
|
+
* given, and a shared fixture would carry one test's write into
|
|
133
|
+
* the next. */
|
|
134
|
+
answer(editor, remote, c, 0, JSON.parse(JSON.stringify(RECORDS[c.kw.topic_name])));
|
|
127
135
|
}
|
|
128
136
|
return asked.length;
|
|
129
137
|
}
|
|
@@ -183,6 +191,11 @@ function errors()
|
|
|
183
191
|
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
function warnings()
|
|
195
|
+
{
|
|
196
|
+
return logged.filter((l) => l.level === "warning").map((l) => l.msg);
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
describe("a drop while LOADING", () => {
|
|
187
200
|
|
|
188
201
|
test("the editor leaves ST_LOADING, and the reconnect loads again", () => {
|
|
@@ -276,9 +289,169 @@ describe("a drop while SAVING", () => {
|
|
|
276
289
|
topics: ["topics^db.users^cols"]});
|
|
277
290
|
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
278
291
|
|
|
292
|
+
/* ...and it says the first one was DONE, so once the second ends
|
|
293
|
+
* the model is read again (late_write_done()). */
|
|
279
294
|
answer(editor, remote, second, 0, {id: "db.users.name", value: "name", order: 2,
|
|
280
295
|
type: "string", topics: ["topics^db.users^cols"]});
|
|
296
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
297
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
298
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
299
|
+
expect(errors()).toEqual([]);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* The regression of 7.25.6: a navigation DURING a reload. The load was
|
|
305
|
+
* matched by its round AND by the state being ST_LOADING, and EV_SHOW
|
|
306
|
+
* moved the editor out of ST_LOADING while its three `nodes` were in
|
|
307
|
+
* flight. Every answer was then "for a load that is over", the reload
|
|
308
|
+
* never landed, and request_model() had already emptied the records: the
|
|
309
|
+
* next write patched an empty store and the model lost every treedb.
|
|
310
|
+
*/
|
|
311
|
+
describe("a navigation DURING a reload", () => {
|
|
312
|
+
|
|
313
|
+
test("refresh, move, write: the model keeps every treedb", () => {
|
|
314
|
+
const {editor, remote, host} = build("n1", true);
|
|
315
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
316
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
317
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
318
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
319
|
+
|
|
320
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
321
|
+
expect(editor.priv.pending).toBe(0);
|
|
322
|
+
|
|
323
|
+
const write = start_a_write(editor, host);
|
|
324
|
+
answer(editor, remote, write, 0, {id: "db.users.name", value: "name", order: 2,
|
|
325
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
326
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
327
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
328
|
+
expect(editor.priv.model.treedbs[0].topics[0].cols.length).toBe(2);
|
|
329
|
+
expect(warnings()).toEqual([]);
|
|
330
|
+
expect(errors()).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("the editor stays in ST_LOADING, and lands where the host said last", () => {
|
|
334
|
+
const {editor, remote, host} = build("n2", true);
|
|
335
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
336
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
337
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
338
|
+
|
|
339
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
340
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
341
|
+
expect(editor.priv.treedb_id).toBe("db");
|
|
342
|
+
expect(editor.priv.topic_name).toBe("");
|
|
343
|
+
expect(warnings()).toEqual([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("the reload of a reconnect, with a move in the middle", () => {
|
|
347
|
+
const {editor, remote, host} = build("n3", true);
|
|
348
|
+
start_a_write(editor, host);
|
|
349
|
+
drop(editor, remote, host); /* the reconnect will reload */
|
|
350
|
+
shown.length = 0;
|
|
351
|
+
reconnect(editor, remote, host);
|
|
352
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
353
|
+
|
|
354
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
355
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
356
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
357
|
+
expect(editor.priv.pending).toBe(0);
|
|
358
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
359
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
360
|
+
expect(errors()).toEqual([]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("a drop after the move: the move is applied on the model kept, and the reconnect reloads", () => {
|
|
364
|
+
const {editor, remote, host} = build("n4", true);
|
|
365
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
366
|
+
take("nodes"); /* never answered */
|
|
367
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
368
|
+
|
|
369
|
+
drop(editor, remote, host);
|
|
370
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
371
|
+
|
|
372
|
+
reconnect(editor, remote, host);
|
|
373
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
374
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
375
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
376
|
+
expect(errors()).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("a position with no model and no load", () => {
|
|
381
|
+
|
|
382
|
+
test("does not claim a load, and the session loads it", () => {
|
|
383
|
+
const remote = gobj_create_service("p1_remote", "C_TEST_REMOTE", {}, yuno);
|
|
384
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
385
|
+
const host = gobj_create("p1_host", "C_TEST_EDITOR_HOST", {}, yuno);
|
|
386
|
+
const editor = gobj_create("p1_editor", "C_YUI_SCHEMA_EDITOR", {
|
|
387
|
+
gobj_remote_yuno: remote,
|
|
388
|
+
treedb_name: "treedb_system_schema",
|
|
389
|
+
base_route: "/schemas"
|
|
390
|
+
}, host);
|
|
391
|
+
gobj_start(editor); /* its load cannot leave */
|
|
392
|
+
logged.length = 0;
|
|
393
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
394
|
+
|
|
395
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
396
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
397
|
+
|
|
398
|
+
reconnect(editor, remote, host);
|
|
399
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
281
400
|
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
282
401
|
expect(errors()).toEqual([]);
|
|
283
402
|
});
|
|
284
403
|
});
|
|
404
|
+
|
|
405
|
+
describe("the drop is settled ONCE", () => {
|
|
406
|
+
|
|
407
|
+
test("a load cut by the drop: the transport's three failures are one event", () => {
|
|
408
|
+
const {editor, remote, host} = build("o1", true);
|
|
409
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
410
|
+
const cut = take("nodes");
|
|
411
|
+
expect(cut.length).toBe(3);
|
|
412
|
+
|
|
413
|
+
/* A routing adapter settles all three when its session closes,
|
|
414
|
+
* before (or after) the host's edge. */
|
|
415
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
416
|
+
for(const c of cut) {
|
|
417
|
+
answer(editor, remote, c, -1, null, "the connection dropped");
|
|
418
|
+
}
|
|
419
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
420
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
421
|
+
expect(warnings().length).toBe(1);
|
|
422
|
+
expect(warnings()[0]).toMatch(/dropped during the load/);
|
|
423
|
+
expect(errors()).toEqual([]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("the host's edge first: the failures that follow are not said again", () => {
|
|
427
|
+
const {editor, remote, host} = build("o2", true);
|
|
428
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
429
|
+
const cut = take("nodes");
|
|
430
|
+
|
|
431
|
+
drop(editor, remote, host);
|
|
432
|
+
for(const c of cut) {
|
|
433
|
+
answer(editor, remote, c, -1, null, "the connection dropped");
|
|
434
|
+
}
|
|
435
|
+
expect(warnings().length).toBe(1);
|
|
436
|
+
expect(errors()).toEqual([]);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("a write answered after it was given up", () => {
|
|
441
|
+
|
|
442
|
+
test("it was DONE: the model is read again", () => {
|
|
443
|
+
const {editor, remote, host} = build("w1", true);
|
|
444
|
+
const write = start_a_write(editor, host);
|
|
445
|
+
drop(editor, remote, host);
|
|
446
|
+
reconnect(editor, remote, host);
|
|
447
|
+
answer_the_load(editor, remote);
|
|
448
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
449
|
+
|
|
450
|
+
/* The treedb says it wrote it, after all. */
|
|
451
|
+
answer(editor, remote, write, 0, {id: "db.users.name", value: "name",
|
|
452
|
+
topics: ["topics^db.users^cols"]});
|
|
453
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
454
|
+
expect(take("nodes").length).toBe(3);
|
|
455
|
+
expect(errors()).toEqual([]);
|
|
456
|
+
});
|
|
457
|
+
});
|