@yuneta/gobj-ui 7.25.3 → 7.25.5
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 -1
- package/dist/gobj-ui.cjs.js +205 -48
- package/dist/gobj-ui.es.js +206 -49
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +12 -0
- package/src/c_yui_schema_editor.js +42 -23
- package/src/c_yui_shell.js +61 -8
- package/src/c_yui_treedb_topic_with_form.js +20 -1
- package/src/c_yui_treedb_topics.js +41 -10
- package/src/form_time_value.js +23 -0
- package/src/form_writes_in_flight.js +32 -13
- package/src/form_writes_in_flight.test.js +18 -3
- package/src/host_drafts.js +28 -2
- package/src/host_drafts.test.js +24 -1
- package/src/schema_editor_drafts.wiring.test.js +176 -0
- package/src/schema_version_bump.test.js +10 -5
- package/src/treedb_topics_transport.wiring.test.js +228 -0
- package/src/treedb_write_plan.js +32 -1
- package/src/treedb_write_plan.test.js +22 -1
package/src/host_drafts.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {describe, it, expect} from "vitest";
|
|
2
|
-
import {mark_host_drafts, drafts_of_saved_answer} from "./host_drafts.js";
|
|
2
|
+
import {host_draft_ids, mark_host_drafts, drafts_of_saved_answer} from "./host_drafts.js";
|
|
3
3
|
|
|
4
4
|
const model = {
|
|
5
5
|
treedbs: [
|
|
@@ -60,3 +60,26 @@ describe("drafts_of_saved_answer", () => {
|
|
|
60
60
|
expect(drafts_of_saved_answer(null)).toEqual({});
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
describe("host_draft_ids", () => {
|
|
65
|
+
|
|
66
|
+
it("resolves the host's names on the model, as topic ids", () => {
|
|
67
|
+
expect(host_draft_ids(model, {treedb_a: ["users"], treedb_b: ["users"]}))
|
|
68
|
+
.toEqual({"treedb_a^users": true, "treedb_b^users": true});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is a fresh map every time: what the host stopped naming is gone (M1)", () => {
|
|
72
|
+
/* mark_host_drafts() only ADDS, and the editor fed it the same map
|
|
73
|
+
* every time: a topic the host named before the Save stayed a
|
|
74
|
+
* draft after it. */
|
|
75
|
+
const before = host_draft_ids(model, {treedb_a: ["users", "roles"]});
|
|
76
|
+
const after = host_draft_ids(model, {treedb_a: []});
|
|
77
|
+
expect(before).toEqual({"treedb_a^users": true, "treedb_a^roles": true});
|
|
78
|
+
expect(after).toEqual({});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("no model, no drafts: empty", () => {
|
|
82
|
+
expect(host_draft_ids(null, {treedb_a: ["users"]})).toEqual({});
|
|
83
|
+
expect(host_draft_ids(model, null)).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_editor_drafts.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* EV_DRAFTS driven through a real C_YUI_SCHEMA_EDITOR on a document
|
|
5
|
+
* double. The helper was right and tested; the ACTION folded the
|
|
6
|
+
* host's marks into the session's, so a topic the host named before
|
|
7
|
+
* a Save stayed a draft after it (M1 of the 2026-09-23 review).
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2026, ArtGins.
|
|
10
|
+
* All Rights Reserved.
|
|
11
|
+
***********************************************************************/
|
|
12
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
13
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
14
|
+
|
|
15
|
+
install_dom_double();
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
SDATA_END,
|
|
19
|
+
gclass_create,
|
|
20
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
21
|
+
gobj_start, gobj_send_event, gobj_read_attr,
|
|
22
|
+
gobj_change_state,
|
|
23
|
+
set_log_callback,
|
|
24
|
+
} = await import("@yuneta/gobj-js");
|
|
25
|
+
const {register_c_yui_schema_editor, topic_is_draft} = await import("./c_yui_schema_editor.js");
|
|
26
|
+
|
|
27
|
+
const logged = [];
|
|
28
|
+
const commands = [];
|
|
29
|
+
|
|
30
|
+
/* The yuno's system schema: one treedb, two topics. */
|
|
31
|
+
const RECORDS = {
|
|
32
|
+
treedbs: [{id: "db", schema_version: 4}],
|
|
33
|
+
topics: [
|
|
34
|
+
{id: "db.users", value: "users", order: 1, pkey: "id", topic_version: 2,
|
|
35
|
+
treedbs: ["treedbs^db^topics"]},
|
|
36
|
+
{id: "db.roles", value: "roles", order: 2, pkey: "id", topic_version: 1,
|
|
37
|
+
treedbs: ["treedbs^db^topics"]},
|
|
38
|
+
],
|
|
39
|
+
cols: [
|
|
40
|
+
{id: "db.users.id", value: "id", order: 1, type: "string",
|
|
41
|
+
flag: ["persistent", "required"], topics: ["topics^db.users^cols"]},
|
|
42
|
+
{id: "db.roles.id", value: "id", order: 1, type: "string",
|
|
43
|
+
flag: ["persistent", "required"], topics: ["topics^db.roles^cols"]},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function remote_command_parser(gobj, command, kw, src)
|
|
48
|
+
{
|
|
49
|
+
commands.push({command, kw, src});
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let yuno = null;
|
|
54
|
+
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
57
|
+
set_log_callback((level, msg) => {
|
|
58
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
59
|
+
});
|
|
60
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
61
|
+
gclass_create(
|
|
62
|
+
"C_TEST_EDITOR_HOST",
|
|
63
|
+
[["EV_POSITION_CHANGED", 0], ["EV_RECORD_WRITTEN", 0], ["EV_SCHEMA_CHECKED", 0]],
|
|
64
|
+
[["ST_IDLE", [
|
|
65
|
+
["EV_POSITION_CHANGED", () => 0, null],
|
|
66
|
+
["EV_RECORD_WRITTEN", () => 0, null],
|
|
67
|
+
["EV_SCHEMA_CHECKED", () => 0, null]
|
|
68
|
+
]]],
|
|
69
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
70
|
+
);
|
|
71
|
+
gclass_create(
|
|
72
|
+
"C_TEST_REMOTE",
|
|
73
|
+
[],
|
|
74
|
+
[["ST_SESSION", []]],
|
|
75
|
+
{mt_command_parser: remote_command_parser},
|
|
76
|
+
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
77
|
+
);
|
|
78
|
+
register_c_yui_schema_editor();
|
|
79
|
+
yuno = gobj_create_yuno("drafts_yuno", "C_TEST_HOST", {});
|
|
80
|
+
gobj_start(yuno);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
logged.length = 0;
|
|
85
|
+
commands.length = 0;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/* Answer every `nodes` the editor asked, the way a transport does: the
|
|
89
|
+
* request's frame on top of the command_stack. */
|
|
90
|
+
function answer_the_load(editor, remote)
|
|
91
|
+
{
|
|
92
|
+
const asked = commands.filter((c) => c.command === "nodes");
|
|
93
|
+
commands.length = 0;
|
|
94
|
+
for(const c of asked) {
|
|
95
|
+
gobj_send_event(editor, "EV_MT_COMMAND_ANSWER", {
|
|
96
|
+
result: 0,
|
|
97
|
+
data: RECORDS[c.kw.topic_name],
|
|
98
|
+
__md_iev__: {command_stack: [{command: "nodes", kw: c.kw}]}
|
|
99
|
+
}, remote);
|
|
100
|
+
}
|
|
101
|
+
return asked.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function build(name)
|
|
105
|
+
{
|
|
106
|
+
const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
|
|
107
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
108
|
+
const host = gobj_create(`${name}_host`, "C_TEST_EDITOR_HOST", {}, yuno);
|
|
109
|
+
const editor = gobj_create(`${name}_editor`, "C_YUI_SCHEMA_EDITOR", {
|
|
110
|
+
gobj_remote_yuno: remote,
|
|
111
|
+
treedb_name: "treedb_system_schema",
|
|
112
|
+
base_route: "/schemas"
|
|
113
|
+
}, host);
|
|
114
|
+
gobj_start(editor);
|
|
115
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
116
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
117
|
+
return {editor, remote, host};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function drafts_on_screen(editor)
|
|
121
|
+
{
|
|
122
|
+
const $c = gobj_read_attr(editor, "$container");
|
|
123
|
+
return $c.querySelectorAll(".SCHEMA_TOPIC_VERSION .is-warning").length;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function is_draft(editor, topic_id)
|
|
127
|
+
{
|
|
128
|
+
return topic_is_draft(editor.priv, {id: topic_id});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("EV_DRAFTS: the host is the truth", () => {
|
|
132
|
+
|
|
133
|
+
test("the host names a topic: it is a draft, on screen too", () => {
|
|
134
|
+
const {editor, host} = build("d1");
|
|
135
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {db: ["users"]}}, host);
|
|
136
|
+
expect(is_draft(editor, "db.users")).toBe(true);
|
|
137
|
+
expect(is_draft(editor, "db.roles")).toBe(false);
|
|
138
|
+
expect(drafts_on_screen(editor)).toBe(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the host names none any more (a Save): the mark goes (M1)", () => {
|
|
142
|
+
/* It only ever ADDED: after the Save the editor still said
|
|
143
|
+
* "unsaved schema changes". */
|
|
144
|
+
const {editor, host} = build("d2");
|
|
145
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {db: ["users", "roles"]}}, host);
|
|
146
|
+
expect(drafts_on_screen(editor)).toBe(2);
|
|
147
|
+
|
|
148
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {db: []}}, host);
|
|
149
|
+
expect(is_draft(editor, "db.users")).toBe(false);
|
|
150
|
+
expect(is_draft(editor, "db.roles")).toBe(false);
|
|
151
|
+
expect(drafts_on_screen(editor)).toBe(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("a reload keeps what the host said last, and only that", () => {
|
|
155
|
+
const {editor, remote, host} = build("d3");
|
|
156
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {db: ["roles"]}}, host);
|
|
157
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
158
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
159
|
+
expect(is_draft(editor, "db.roles")).toBe(true);
|
|
160
|
+
expect(is_draft(editor, "db.users")).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("what this session wrote is not cleared by the host's answer", () => {
|
|
164
|
+
const {editor, host} = build("d4");
|
|
165
|
+
editor.priv.written["db.users"] = true; /* a write of this session */
|
|
166
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {}}, host);
|
|
167
|
+
expect(is_draft(editor, "db.users")).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("no error logged on the way", () => {
|
|
171
|
+
const {editor, host} = build("d5");
|
|
172
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {db: ["users"]}}, host);
|
|
173
|
+
gobj_send_event(editor, "EV_DRAFTS", {drafts: {}}, host);
|
|
174
|
+
expect(logged.filter((l) => l.level === "error").map((l) => l.msg)).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -19,17 +19,22 @@ const TOPIC = {id: "device_groups", topic_version: 5};
|
|
|
19
19
|
|
|
20
20
|
describe("topic_is_draft", () => {
|
|
21
21
|
it("says nothing about a topic this session never wrote", () => {
|
|
22
|
-
expect(topic_is_draft({}, TOPIC)).toBe(false);
|
|
22
|
+
expect(topic_is_draft({written: {}, host_ids: {}}, TOPIC)).toBe(false);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it("a written topic is a draft, whatever its version says", () => {
|
|
26
|
-
expect(topic_is_draft({device_groups: true}, TOPIC)).toBe(true);
|
|
27
|
-
expect(topic_is_draft({
|
|
28
|
-
.toBe(true);
|
|
26
|
+
expect(topic_is_draft({written: {device_groups: true}}, TOPIC)).toBe(true);
|
|
27
|
+
expect(topic_is_draft({written: {device_groups: true}},
|
|
28
|
+
{id: "device_groups", topic_version: 6})).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("a topic the host names is a draft too (EV_DRAFTS)", () => {
|
|
32
|
+
expect(topic_is_draft({written: {}, host_ids: {device_groups: true}}, TOPIC)).toBe(true);
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
it("answers false rather than throwing on a missing half", () => {
|
|
32
36
|
expect(topic_is_draft(null, TOPIC)).toBe(false);
|
|
33
|
-
expect(topic_is_draft({device_groups: true}, null)).toBe(false);
|
|
37
|
+
expect(topic_is_draft({written: {device_groups: true}}, null)).toBe(false);
|
|
38
|
+
expect(topic_is_draft({}, TOPIC)).toBe(false);
|
|
34
39
|
});
|
|
35
40
|
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_topics_transport.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* The WIRING of a form's write and the backend going away, driven
|
|
5
|
+
* through the real gclasses (C_YUI_SHELL, C_YUI_TREEDB_TOPICS) on a
|
|
6
|
+
* document double. The helper (form_writes_in_flight.js) was tested
|
|
7
|
+
* and right; what failed was that two hosts never called the action
|
|
8
|
+
* that uses it (M8 of the 2026-09-23 review), which no helper test
|
|
9
|
+
* can see.
|
|
10
|
+
*
|
|
11
|
+
* Copyright (c) 2026, ArtGins.
|
|
12
|
+
* All Rights Reserved.
|
|
13
|
+
***********************************************************************/
|
|
14
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
15
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
16
|
+
|
|
17
|
+
install_dom_double();
|
|
18
|
+
|
|
19
|
+
const gobj_js = await import("@yuneta/gobj-js");
|
|
20
|
+
const {
|
|
21
|
+
SDATA, SDATA_END, data_type_t, event_flag_t,
|
|
22
|
+
gclass_create,
|
|
23
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
24
|
+
gobj_start, gobj_send_event,
|
|
25
|
+
gobj_change_state,
|
|
26
|
+
set_log_callback,
|
|
27
|
+
} = gobj_js;
|
|
28
|
+
const {register_c_yui_shell, yui_shell_set_connection_state} = await import("./c_yui_shell.js");
|
|
29
|
+
const {register_c_yui_treedb_topics} = await import("./c_yui_treedb_topics.js");
|
|
30
|
+
|
|
31
|
+
const logged = [];
|
|
32
|
+
const answers = []; /* what the fake forms were told */
|
|
33
|
+
const commands = []; /* what the fake transport was asked */
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The transport: answers nothing, like a C_IEVENT_CLI whose socket is
|
|
37
|
+
* about to drop. Its state is what `is_connected()` reads.
|
|
38
|
+
*/
|
|
39
|
+
function remote_command_parser(gobj, command, kw, src)
|
|
40
|
+
{
|
|
41
|
+
commands.push({command, kw});
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* A topic form, as C_YUI_TREEDB_TOPICS finds it (by gclass name and
|
|
47
|
+
* `topic_name`) to answer its write.
|
|
48
|
+
*/
|
|
49
|
+
function ac_form_answer(gobj, event, kw, src)
|
|
50
|
+
{
|
|
51
|
+
answers.push({topic: gobj.priv_topic, event, form_write: kw.form_write});
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let yuno = null;
|
|
56
|
+
let remote = null;
|
|
57
|
+
|
|
58
|
+
beforeAll(() => {
|
|
59
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
60
|
+
set_log_callback((level, msg) => {
|
|
61
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
65
|
+
gclass_create(
|
|
66
|
+
"C_TEST_REMOTE",
|
|
67
|
+
[],
|
|
68
|
+
[["ST_SESSION", []], ["ST_DISCONNECTED", []]],
|
|
69
|
+
{mt_command_parser: remote_command_parser},
|
|
70
|
+
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
71
|
+
);
|
|
72
|
+
gclass_create(
|
|
73
|
+
"C_YUI_TREEDB_TOPIC_WITH_FORM",
|
|
74
|
+
[["EV_WRITE_DONE", 0], ["EV_WRITE_REFUSED", 0]],
|
|
75
|
+
[["ST_IDLE", [
|
|
76
|
+
["EV_WRITE_DONE", ac_form_answer, null],
|
|
77
|
+
["EV_WRITE_REFUSED", ac_form_answer, null]
|
|
78
|
+
]]],
|
|
79
|
+
{},
|
|
80
|
+
0,
|
|
81
|
+
[SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "topic"), SDATA_END()],
|
|
82
|
+
{}, 0, 0, 0, 0
|
|
83
|
+
);
|
|
84
|
+
/* The host of the view: it hears what a CHILD view publishes. */
|
|
85
|
+
gclass_create(
|
|
86
|
+
"C_TEST_VIEW_HOST",
|
|
87
|
+
[["EV_RECORD_WRITTEN", 0], ["EV_TOPIC_SELECTED", 0]],
|
|
88
|
+
[["ST_IDLE", [
|
|
89
|
+
["EV_RECORD_WRITTEN", () => 0, null],
|
|
90
|
+
["EV_TOPIC_SELECTED", () => 0, null]
|
|
91
|
+
]]],
|
|
92
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
93
|
+
);
|
|
94
|
+
/* An app that subscribes to EVERY event of its shell and declares
|
|
95
|
+
* none of the new ones: wattyzer's and yunovatios' shape. */
|
|
96
|
+
gclass_create("C_TEST_APP", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
97
|
+
|
|
98
|
+
register_c_yui_shell();
|
|
99
|
+
register_c_yui_treedb_topics();
|
|
100
|
+
|
|
101
|
+
yuno = gobj_create_yuno("wiring_yuno", "C_TEST_HOST", {});
|
|
102
|
+
gobj_start(yuno);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
logged.length = 0;
|
|
107
|
+
answers.length = 0;
|
|
108
|
+
commands.length = 0;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function build(name)
|
|
112
|
+
{
|
|
113
|
+
const app = gobj_create(`${name}_app`, "C_TEST_APP", {}, yuno);
|
|
114
|
+
gobj_start(app);
|
|
115
|
+
const shell = gobj_create(`${name}_shell`, "C_YUI_SHELL", {config: {shell: {}}, default_route: "/home", subscriber: app}, app);
|
|
116
|
+
gobj_start(shell);
|
|
117
|
+
|
|
118
|
+
remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
|
|
119
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
120
|
+
|
|
121
|
+
const host = gobj_create(`${name}_host`, "C_TEST_VIEW_HOST", {}, yuno);
|
|
122
|
+
const topics = gobj_create_service(`${name}_topics`, "C_YUI_TREEDB_TOPICS", {
|
|
123
|
+
treedb_name: "treedb_test",
|
|
124
|
+
gobj_remote_yuno: remote
|
|
125
|
+
}, host);
|
|
126
|
+
gobj_start(topics);
|
|
127
|
+
|
|
128
|
+
const forms = {};
|
|
129
|
+
for(const topic of ["users", "roles"]) {
|
|
130
|
+
forms[topic] = gobj_create(`${name}_form_${topic}`, "C_YUI_TREEDB_TOPIC_WITH_FORM",
|
|
131
|
+
{topic_name: topic}, topics);
|
|
132
|
+
forms[topic].priv_topic = topic;
|
|
133
|
+
}
|
|
134
|
+
/* What the start of a shell with no routes says is the fixture's,
|
|
135
|
+
* not the behaviour under test: the check starts here. */
|
|
136
|
+
logged.length = 0;
|
|
137
|
+
return {app, shell, topics, forms};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function save(topics, form, form_write)
|
|
141
|
+
{
|
|
142
|
+
gobj_send_event(topics, "EV_UPDATE_RECORD",
|
|
143
|
+
{record: {id: "x"}, form_write: form_write}, form);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* EVERY error the run logged: the check a deployed page gets too. */
|
|
147
|
+
function errors()
|
|
148
|
+
{
|
|
149
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe("a Save in flight when the backend drops (M8)", () => {
|
|
153
|
+
|
|
154
|
+
test("the shell's connection edge answers every form, with no host forwarding", () => {
|
|
155
|
+
const {shell, topics, forms} = build("t1");
|
|
156
|
+
yui_shell_set_connection_state(shell, true);
|
|
157
|
+
|
|
158
|
+
/* Two forms, each at ITS serial 1: two writes, not one. */
|
|
159
|
+
save(topics, forms.users, 1);
|
|
160
|
+
save(topics, forms.roles, 1);
|
|
161
|
+
expect(commands.filter((c) => c.command === "update-node").length).toBe(2);
|
|
162
|
+
expect(answers).toEqual([]);
|
|
163
|
+
|
|
164
|
+
yui_shell_set_connection_state(shell, false);
|
|
165
|
+
|
|
166
|
+
expect(answers).toEqual([
|
|
167
|
+
{topic: "users", event: "EV_WRITE_REFUSED", form_write: 1},
|
|
168
|
+
{topic: "roles", event: "EV_WRITE_REFUSED", form_write: 1},
|
|
169
|
+
]);
|
|
170
|
+
expect(errors()).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a host that also forwards EV_TRANSPORT_STATE: each write answered once", () => {
|
|
174
|
+
const {shell, topics, forms} = build("t2");
|
|
175
|
+
yui_shell_set_connection_state(shell, true);
|
|
176
|
+
save(topics, forms.users, 1);
|
|
177
|
+
|
|
178
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: false}, topics);
|
|
179
|
+
yui_shell_set_connection_state(shell, false);
|
|
180
|
+
|
|
181
|
+
expect(answers).toEqual([
|
|
182
|
+
{topic: "users", event: "EV_WRITE_REFUSED", form_write: 1},
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("only an EDGE is published: a repeated 'down' answers nothing twice", () => {
|
|
187
|
+
const {shell, topics, forms} = build("t3");
|
|
188
|
+
yui_shell_set_connection_state(shell, true);
|
|
189
|
+
save(topics, forms.users, 1);
|
|
190
|
+
yui_shell_set_connection_state(shell, false);
|
|
191
|
+
yui_shell_set_connection_state(shell, false);
|
|
192
|
+
expect(answers.length).toBe(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("an app subscribed to every shell event is not sent the new one", () => {
|
|
196
|
+
/* C_TEST_APP declares no event at all: were EV_CONNECTION_STATE
|
|
197
|
+
* published to it, the FSM would log "Event NOT DEFINED". */
|
|
198
|
+
const {shell} = build("t4");
|
|
199
|
+
yui_shell_set_connection_state(shell, true);
|
|
200
|
+
yui_shell_set_connection_state(shell, false);
|
|
201
|
+
expect(errors()).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("an answer from the backend settles its own form only", () => {
|
|
205
|
+
const {shell, topics, forms} = build("t5");
|
|
206
|
+
yui_shell_set_connection_state(shell, true);
|
|
207
|
+
save(topics, forms.users, 1);
|
|
208
|
+
save(topics, forms.roles, 1);
|
|
209
|
+
|
|
210
|
+
/* The answer of the users write, the way C_IEVENT_CLI delivers it:
|
|
211
|
+
* the request's frame on top of the command_stack. */
|
|
212
|
+
const ok = {result: 0, comment: "", data: [{id: "x"}], __md_iev__: {
|
|
213
|
+
command_stack: [{command: "update-node", kw: {
|
|
214
|
+
topic_name: "users", treedb_name: "treedb_test", form_write: 1, record: {id: "x"}
|
|
215
|
+
}}]
|
|
216
|
+
}};
|
|
217
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", ok, remote);
|
|
218
|
+
expect(answers).toEqual([
|
|
219
|
+
{topic: "users", event: "EV_WRITE_DONE", form_write: 1},
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
yui_shell_set_connection_state(shell, false);
|
|
223
|
+
expect(answers).toEqual([
|
|
224
|
+
{topic: "users", event: "EV_WRITE_DONE", form_write: 1},
|
|
225
|
+
{topic: "roles", event: "EV_WRITE_REFUSED", form_write: 1},
|
|
226
|
+
]);
|
|
227
|
+
});
|
|
228
|
+
});
|
package/src/treedb_write_plan.js
CHANGED
|
@@ -116,4 +116,35 @@ function col_goes_back_to_treedb(desc, col)
|
|
|
116
116
|
return topic_pkey2s(d).includes(col.id);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/************************************************************
|
|
120
|
+
* The INSTANCE an update of `record` is for: its pkey2 values, as the
|
|
121
|
+
* record held them when the form was opened, {pkey2: value}.
|
|
122
|
+
*
|
|
123
|
+
* They go back as they WERE, never as the form's widget hands them: a
|
|
124
|
+
* `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
|
|
125
|
+
* that crossed the widget (seconds dropped, a local wall time read
|
|
126
|
+
* back, an hour the autumn change names twice) names ANOTHER instance
|
|
127
|
+
* -- or none, and C_NODE falls back to the primary. The pkey2 is an
|
|
128
|
+
* address, not a field of the edit.
|
|
129
|
+
************************************************************/
|
|
130
|
+
function instance_keys_of(record, desc)
|
|
131
|
+
{
|
|
132
|
+
const keys = {};
|
|
133
|
+
if(!record || typeof record !== "object") {
|
|
134
|
+
return keys;
|
|
135
|
+
}
|
|
136
|
+
for(const k of topic_pkey2s(desc || {})) {
|
|
137
|
+
if(Object.prototype.hasOwnProperty.call(record, k) &&
|
|
138
|
+
record[k] !== undefined && record[k] !== null) {
|
|
139
|
+
keys[k] = record[k];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return keys;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
plan_treedb_writes,
|
|
147
|
+
READONLY_FORM_TOOLBAR,
|
|
148
|
+
col_goes_back_to_treedb,
|
|
149
|
+
instance_keys_of,
|
|
150
|
+
};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
***********************************************************************/
|
|
10
10
|
import { describe, test, expect } from "vitest";
|
|
11
11
|
import {
|
|
12
|
-
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
12
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb,
|
|
13
|
+
instance_keys_of
|
|
13
14
|
} from "./treedb_write_plan.js";
|
|
14
15
|
|
|
15
16
|
const WRITES = ["edition_mode", "new_button", "delete_button", "paste_button", "in_row_icons"];
|
|
@@ -133,3 +134,23 @@ describe("what goes back to treedb from a form", () => {
|
|
|
133
134
|
expect(col_goes_back_to_treedb(null, COL.yuno_release)).toBe(false);
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
|
|
138
|
+
describe("the instance an update is for", () => {
|
|
139
|
+
test("the pkey2 values as the record held them", () => {
|
|
140
|
+
expect(instance_keys_of({id: "a", yuno_release: "1.2.3", yuno_name: "x"}, YUNOS_DESC))
|
|
141
|
+
.toEqual({yuno_release: "1.2.3"});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a time pkey2 keeps its seconds: the ORIGINAL epoch, not the widget's", () => {
|
|
145
|
+
/* A datetime-local that dropped the seconds, or read a local wall
|
|
146
|
+
* time back, named another instance of the node. */
|
|
147
|
+
const desc = {pkey: "id", pkey2s: ["tm"]};
|
|
148
|
+
expect(instance_keys_of({id: "a", tm: 1790000059}, desc)).toEqual({tm: 1790000059});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a record without the pkey2, or no pkey2s at all: nothing", () => {
|
|
152
|
+
expect(instance_keys_of({id: "a"}, YUNOS_DESC)).toEqual({});
|
|
153
|
+
expect(instance_keys_of({id: "a", yuno_release: "1"}, {pkey: "id"})).toEqual({});
|
|
154
|
+
expect(instance_keys_of(null, YUNOS_DESC)).toEqual({});
|
|
155
|
+
});
|
|
156
|
+
});
|