@loadbare/app 0.5.6 → 0.6.0
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 +3 -4
- package/dist/build/assemble.d.ts +1 -1
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +80 -7
- package/dist/build/cli.d.ts +2 -2
- package/dist/build/cli.js +2 -2
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +19 -32
- package/dist/build/locations.d.ts +3 -3
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +15 -3
- package/dist/build/origins.d.ts +0 -13
- package/dist/build/origins.d.ts.map +1 -1
- package/dist/build/origins.js +32 -8
- package/dist/build/pages.d.ts +3 -3
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +9 -7
- package/dist/core/lb-constants.d.ts +15 -13
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +103 -53
- package/dist/core/lb-types.d.ts +103 -62
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +11 -3
- package/dist/hub/lb-apply.d.ts +18 -4
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +184 -34
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +131 -84
- package/dist/server/lb-express.d.ts +7 -4
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +45 -33
- package/dist/server/lb-server.d.ts +67 -45
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +55 -18
- package/dist/tests/assemble.test.js +154 -2
- package/dist/tests/expand.test.js +6 -3
- package/dist/tests/helpers/hub.d.ts +73 -0
- package/dist/tests/helpers/hub.d.ts.map +1 -0
- package/dist/tests/helpers/hub.js +151 -0
- package/dist/tests/lb-apply.test.js +86 -62
- package/dist/tests/lb-express.test.d.ts +1 -1
- package/dist/tests/lb-express.test.js +43 -38
- package/dist/tests/lb-hub.test.d.ts +14 -0
- package/dist/tests/lb-hub.test.d.ts.map +1 -0
- package/dist/tests/lb-hub.test.js +319 -0
- package/dist/tests/{lb-rows.test.d.ts → lb-list.test.d.ts} +1 -1
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/{lb-rows.test.js → lb-list.test.js} +109 -106
- package/dist/tests/lb-server.test.js +151 -100
- package/dist/tests/origins.test.js +19 -1
- package/dist/tests/pages.test.d.ts +1 -1
- package/dist/tests/pages.test.js +64 -14
- package/docs/TECHREF-1.0.md +1000 -0
- package/docs/reference/builder.md +4 -4
- package/docs/reference/chrome.md +10 -9
- package/docs/reference/custom-elements.md +51 -36
- package/docs/reference/data-binding.md +141 -88
- package/docs/reference/overview.md +1 -1
- package/docs/reference/page-files.md +64 -49
- package/docs/reference/server.md +7 -6
- package/docs/reference/widgets.md +22 -30
- package/docs/roadmap.md +68 -22
- package/docs/testing.md +47 -17
- package/docs/theory.md +2 -2
- package/docs/tutorials/010-pages-and-navigation.md +8 -8
- package/docs/tutorials/040-displaying-data.md +9 -9
- package/docs/tutorials/050-actions.md +5 -5
- package/docs/tutorials/060-custom-element-code.md +1 -1
- package/docs/tutorials/065-conditional-rendering.md +4 -4
- package/docs/tutorials/070-displaying-a-list.md +24 -47
- package/docs/tutorials/072-inserting-into-a-list.md +17 -17
- package/docs/tutorials/074-deleting-from-a-list.md +15 -17
- package/docs/tutorials/076-updating-a-list-item.md +20 -22
- package/docs/tutorials/080-widget-requests.md +27 -23
- package/docs/tutorials/090-using-widget-libraries.md +1 -1
- package/package.json +1 -2
- package/dist/hub/lb-rows.d.ts +0 -18
- package/dist/hub/lb-rows.d.ts.map +0 -1
- package/dist/hub/lb-rows.js +0 -106
- package/dist/tests/lb-rows.test.d.ts.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The Express adapter — see docs/testing.md, tier 2.
|
|
3
3
|
*
|
|
4
4
|
* server/lb-express.ts is the only file that knows both HTTP and Hub. What
|
|
5
|
-
* it owes is small and exact:
|
|
5
|
+
* it owes is small and exact: one endpoint and no second, the page from the
|
|
6
6
|
* query string, and an operation it does not yet handle refused rather than
|
|
7
7
|
* half-handled.
|
|
8
8
|
*
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
14
|
import { after, before, describe, it } from "node:test";
|
|
15
15
|
import express from "express";
|
|
16
|
-
import {
|
|
16
|
+
import { LB_ENDPOINT } from "../core/lb-constants";
|
|
17
17
|
import { hubRoutes } from "../server/lb-express";
|
|
18
18
|
import { captured } from "./helpers/console";
|
|
19
19
|
const calls = [];
|
|
@@ -33,15 +33,15 @@ const engine = {
|
|
|
33
33
|
calls.push({ kind: "cell-change", page, where, ctx });
|
|
34
34
|
return { members: { n: "3" } };
|
|
35
35
|
},
|
|
36
|
-
async
|
|
36
|
+
async runRowDelete(page, where, ctx) {
|
|
37
37
|
calls.push({ kind: "tuple-delete", page, where, ctx });
|
|
38
38
|
return { members: { n: "4" } };
|
|
39
39
|
},
|
|
40
|
-
async
|
|
40
|
+
async runRowInsert(page, where, ctx) {
|
|
41
41
|
calls.push({ kind: "tuple-insert", page, where, ctx });
|
|
42
42
|
return { members: { n: "5" } };
|
|
43
43
|
},
|
|
44
|
-
async
|
|
44
|
+
async runRowUpdate(page, where, ctx) {
|
|
45
45
|
calls.push({ kind: "tuple-update", page, where, ctx });
|
|
46
46
|
return { members: { n: "6" } };
|
|
47
47
|
},
|
|
@@ -61,16 +61,16 @@ before(async () => {
|
|
|
61
61
|
});
|
|
62
62
|
after(() => void server.close());
|
|
63
63
|
function post(page, body, raw) {
|
|
64
|
-
return fetch(`${base}${
|
|
64
|
+
return fetch(`${base}${LB_ENDPOINT}?page=${page}`, {
|
|
65
65
|
method: "POST",
|
|
66
66
|
headers: { "content-type": "application/json" },
|
|
67
67
|
body: raw ?? JSON.stringify(body),
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
describe("
|
|
70
|
+
describe("a request with no action", () => {
|
|
71
71
|
it("runs the page named in the query string", async () => {
|
|
72
72
|
calls.length = 0;
|
|
73
|
-
const res = await
|
|
73
|
+
const res = await post("hello", {});
|
|
74
74
|
assert.equal(res.status, 200);
|
|
75
75
|
assert.deepEqual(await res.json(), { greeting: { text: "hello" } });
|
|
76
76
|
assert.equal(calls.length, 1);
|
|
@@ -81,17 +81,20 @@ describe("the data endpoint", () => {
|
|
|
81
81
|
// The engine answers for a page it does not have; the route's job is only
|
|
82
82
|
// to pass on what it was given rather than to invent a default.
|
|
83
83
|
calls.length = 0;
|
|
84
|
-
await fetch(`${base}${
|
|
84
|
+
await fetch(`${base}${LB_ENDPOINT}`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
body: "{}",
|
|
88
|
+
});
|
|
85
89
|
assert.equal(calls[0].page, "");
|
|
86
90
|
});
|
|
87
91
|
});
|
|
88
|
-
describe("
|
|
92
|
+
describe("a request carrying an action", () => {
|
|
89
93
|
it("runs the action, with where the interaction happened", async () => {
|
|
90
94
|
calls.length = 0;
|
|
91
95
|
const res = await post("roster", {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
query: "members",
|
|
96
|
+
action: "pick",
|
|
97
|
+
list: "members",
|
|
95
98
|
key: "7",
|
|
96
99
|
cell: "role",
|
|
97
100
|
value: "lead",
|
|
@@ -99,7 +102,8 @@ describe("the request endpoint", () => {
|
|
|
99
102
|
assert.equal(res.status, 200);
|
|
100
103
|
assert.deepEqual(await res.json(), { count: { n: "2" } });
|
|
101
104
|
assert.deepEqual(calls[0].where, {
|
|
102
|
-
|
|
105
|
+
list: "members",
|
|
106
|
+
row: undefined,
|
|
103
107
|
key: "7",
|
|
104
108
|
cell: "role",
|
|
105
109
|
value: "lead",
|
|
@@ -111,27 +115,28 @@ describe("the request endpoint", () => {
|
|
|
111
115
|
// A button carries no value and may sit outside any scope. Undefined
|
|
112
116
|
// stays undefined rather than becoming the empty string.
|
|
113
117
|
calls.length = 0;
|
|
114
|
-
await post("counter", {
|
|
118
|
+
await post("counter", { action: "bump" });
|
|
115
119
|
assert.deepEqual(calls[0].where, {
|
|
116
|
-
|
|
120
|
+
list: undefined,
|
|
121
|
+
row: undefined,
|
|
117
122
|
key: undefined,
|
|
118
123
|
cell: undefined,
|
|
119
124
|
value: undefined,
|
|
120
125
|
});
|
|
121
126
|
});
|
|
122
|
-
it("refuses
|
|
127
|
+
it("refuses a reserved name it does not implement, rather than guessing", async () => {
|
|
123
128
|
calls.length = 0;
|
|
124
|
-
const { result: res, warn } = await captured(() => post("roster", {
|
|
129
|
+
const { result: res, warn } = await captured(() => post("roster", { action: "lb-row-upsert", list: "members", key: "1" }));
|
|
125
130
|
assert.equal(res.status, 400);
|
|
126
131
|
assert.deepEqual(await res.json(), {});
|
|
127
132
|
assert.deepEqual(calls, [], "the engine was never reached");
|
|
128
|
-
assert.match(warn.join("\n"), /operation '
|
|
133
|
+
assert.match(warn.join("\n"), /operation 'lb-row-upsert' not implemented/);
|
|
129
134
|
});
|
|
130
135
|
it("runs a cell-change, with where the interaction happened", async () => {
|
|
131
136
|
calls.length = 0;
|
|
132
137
|
const res = await post("roster", {
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
action: "lb-cell-change",
|
|
139
|
+
list: "members",
|
|
135
140
|
key: "7",
|
|
136
141
|
cell: "name",
|
|
137
142
|
value: "Ada",
|
|
@@ -139,7 +144,7 @@ describe("the request endpoint", () => {
|
|
|
139
144
|
assert.equal(res.status, 200);
|
|
140
145
|
assert.deepEqual(await res.json(), { members: { n: "3" } });
|
|
141
146
|
assert.deepEqual(calls[0].where, {
|
|
142
|
-
|
|
147
|
+
list: "members",
|
|
143
148
|
key: "7",
|
|
144
149
|
cell: "name",
|
|
145
150
|
value: "Ada",
|
|
@@ -150,27 +155,27 @@ describe("the request endpoint", () => {
|
|
|
150
155
|
it("runs a tuple-delete, with the key of the row", async () => {
|
|
151
156
|
calls.length = 0;
|
|
152
157
|
const res = await post("roster", {
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
action: "lb-row-delete",
|
|
159
|
+
list: "members",
|
|
155
160
|
key: "7",
|
|
156
161
|
});
|
|
157
162
|
assert.equal(res.status, 200);
|
|
158
163
|
assert.deepEqual(await res.json(), { members: { n: "4" } });
|
|
159
|
-
assert.deepEqual(calls[0].where, {
|
|
164
|
+
assert.deepEqual(calls[0].where, { list: "members", key: "7" });
|
|
160
165
|
assert.equal(calls[0].kind, "tuple-delete");
|
|
161
166
|
assert.equal(calls[0].page, "roster");
|
|
162
167
|
});
|
|
163
168
|
it("runs a tuple-insert, with the gathered values", async () => {
|
|
164
169
|
calls.length = 0;
|
|
165
170
|
const res = await post("roster", {
|
|
166
|
-
|
|
167
|
-
|
|
171
|
+
action: "lb-row-insert",
|
|
172
|
+
list: "members",
|
|
168
173
|
values: { name: "Ada", role: "Analyst" },
|
|
169
174
|
});
|
|
170
175
|
assert.equal(res.status, 200);
|
|
171
176
|
assert.deepEqual(await res.json(), { members: { n: "5" } });
|
|
172
177
|
assert.deepEqual(calls[0].where, {
|
|
173
|
-
|
|
178
|
+
list: "members",
|
|
174
179
|
values: { name: "Ada", role: "Analyst" },
|
|
175
180
|
});
|
|
176
181
|
assert.equal(calls[0].kind, "tuple-insert");
|
|
@@ -179,15 +184,15 @@ describe("the request endpoint", () => {
|
|
|
179
184
|
it("runs a tuple-update, with the key and the gathered values", async () => {
|
|
180
185
|
calls.length = 0;
|
|
181
186
|
const res = await post("roster", {
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
action: "lb-row-update",
|
|
188
|
+
list: "members",
|
|
184
189
|
key: "7",
|
|
185
190
|
values: { name: "Ada", role: "Analyst" },
|
|
186
191
|
});
|
|
187
192
|
assert.equal(res.status, 200);
|
|
188
193
|
assert.deepEqual(await res.json(), { members: { n: "6" } });
|
|
189
194
|
assert.deepEqual(calls[0].where, {
|
|
190
|
-
|
|
195
|
+
list: "members",
|
|
191
196
|
key: "7",
|
|
192
197
|
values: { name: "Ada", role: "Analyst" },
|
|
193
198
|
});
|
|
@@ -202,12 +207,12 @@ describe("the request endpoint", () => {
|
|
|
202
207
|
});
|
|
203
208
|
it("survives an operation that throws, rather than crashing", async () => {
|
|
204
209
|
calls.length = 0;
|
|
205
|
-
const { result: res, error } = await captured(() => post("roster", {
|
|
210
|
+
const { result: res, error } = await captured(() => post("roster", { action: "explode", list: "members" }));
|
|
206
211
|
assert.equal(res.status, 500);
|
|
207
212
|
assert.deepEqual(await res.json(), {});
|
|
208
|
-
assert.match(error.join("\n"), /
|
|
213
|
+
assert.match(error.join("\n"), /'explode' threw/);
|
|
209
214
|
// And the process is still serving.
|
|
210
|
-
const next = await
|
|
215
|
+
const next = await post("hello", {});
|
|
211
216
|
assert.equal(next.status, 200);
|
|
212
217
|
});
|
|
213
218
|
it("survives a body that is not JSON", async () => {
|
|
@@ -217,7 +222,7 @@ describe("the request endpoint", () => {
|
|
|
217
222
|
assert.deepEqual(calls, []);
|
|
218
223
|
// And the process is still serving. A parse failure is one request's
|
|
219
224
|
// problem, not the server's.
|
|
220
|
-
const next = await
|
|
225
|
+
const next = await post("hello", {});
|
|
221
226
|
assert.equal(next.status, 200);
|
|
222
227
|
});
|
|
223
228
|
});
|
|
@@ -228,9 +233,9 @@ describe("the context", () => {
|
|
|
228
233
|
// process is the mistake this asserts against.
|
|
229
234
|
contexts = 0;
|
|
230
235
|
calls.length = 0;
|
|
231
|
-
await
|
|
232
|
-
await
|
|
233
|
-
await post("counter", {
|
|
236
|
+
await post("hello", {});
|
|
237
|
+
await post("hello", {});
|
|
238
|
+
await post("counter", { action: "bump" });
|
|
234
239
|
assert.equal(contexts, 3);
|
|
235
240
|
assert.notEqual(calls[0].ctx, calls[1].ctx);
|
|
236
241
|
assert.notEqual(calls[1].ctx, calls[2].ctx);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub element — see hub/lb-hub.browser.ts and docs/testing.md.
|
|
3
|
+
*
|
|
4
|
+
* Tier 4. jsdom is evidence for what the hub does to the document it is
|
|
5
|
+
* already holding: which element an event came from, what request that
|
|
6
|
+
* produces, which attributes land where, and which page host a path selects.
|
|
7
|
+
* A real network and a real frame belong to the browser tier, which is not
|
|
8
|
+
* built.
|
|
9
|
+
*
|
|
10
|
+
* The hub registers itself as its module loads, so the window goes in first
|
|
11
|
+
* and the module is imported after it.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=lb-hub.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-hub.test.d.ts","sourceRoot":"","sources":["../../tests/lb-hub.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub element — see hub/lb-hub.browser.ts and docs/testing.md.
|
|
3
|
+
*
|
|
4
|
+
* Tier 4. jsdom is evidence for what the hub does to the document it is
|
|
5
|
+
* already holding: which element an event came from, what request that
|
|
6
|
+
* produces, which attributes land where, and which page host a path selects.
|
|
7
|
+
* A real network and a real frame belong to the browser tier, which is not
|
|
8
|
+
* built.
|
|
9
|
+
*
|
|
10
|
+
* The hub registers itself as its module loads, so the window goes in first
|
|
11
|
+
* and the module is imported after it.
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { before, describe, it } from "node:test";
|
|
15
|
+
import { captured } from "./helpers/console";
|
|
16
|
+
import { back, click, installWindow, mountHub, send, server, settle, submit, } from "./helpers/hub";
|
|
17
|
+
installWindow();
|
|
18
|
+
before(async () => {
|
|
19
|
+
await import("../hub/lb-hub.browser");
|
|
20
|
+
});
|
|
21
|
+
/** A live row, as the hub stamps one. */
|
|
22
|
+
const ROSTER = `
|
|
23
|
+
<ul lb-list="roster">
|
|
24
|
+
<li lb-key-value="42">
|
|
25
|
+
<span lb-cell="name">Ann</span>
|
|
26
|
+
<button lb-action="lb-row-delete">x</button>
|
|
27
|
+
</li>
|
|
28
|
+
<template lb-key="id"><li><span lb-cell="name"></span></li></template>
|
|
29
|
+
</ul>`;
|
|
30
|
+
describe("what a native element asks for", () => {
|
|
31
|
+
it("builds a delete from the list and the key around the button", async () => {
|
|
32
|
+
const hub = await mountHub(ROSTER);
|
|
33
|
+
server.sent.length = 0;
|
|
34
|
+
await click(hub.querySelector("button"));
|
|
35
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
36
|
+
action: "lb-row-delete",
|
|
37
|
+
list: "roster",
|
|
38
|
+
key: "42",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it("builds an insert from a form in a list scope, gathering its cells", async () => {
|
|
42
|
+
const hub = await mountHub(`
|
|
43
|
+
<form lb-list="roster" lb-action="lb-row-insert">
|
|
44
|
+
<input lb-cell="name" value="Bo" />
|
|
45
|
+
</form>`);
|
|
46
|
+
server.sent.length = 0;
|
|
47
|
+
await submit(hub.querySelector("form"));
|
|
48
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
49
|
+
action: "lb-row-insert",
|
|
50
|
+
list: "roster",
|
|
51
|
+
values: { name: "Bo" },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("builds an update from a form inside a live row", async () => {
|
|
55
|
+
const hub = await mountHub(`
|
|
56
|
+
<ul lb-list="roster">
|
|
57
|
+
<li lb-key-value="42">
|
|
58
|
+
<form lb-action="lb-row-update">
|
|
59
|
+
<input lb-cell="name" value="Ann B" />
|
|
60
|
+
</form>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>`);
|
|
63
|
+
server.sent.length = 0;
|
|
64
|
+
await submit(hub.querySelector("form"));
|
|
65
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
66
|
+
action: "lb-row-update",
|
|
67
|
+
list: "roster",
|
|
68
|
+
key: "42",
|
|
69
|
+
values: { name: "Ann B" },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("carries whatever binding is in scope for a declared action, and no value", async () => {
|
|
73
|
+
const hub = await mountHub(`
|
|
74
|
+
<section lb-row="prefs">
|
|
75
|
+
<button lb-action="reset" lb-cell="active_tab">Reset</button>
|
|
76
|
+
</section>`);
|
|
77
|
+
server.sent.length = 0;
|
|
78
|
+
await click(hub.querySelector("button"));
|
|
79
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
80
|
+
action: "reset",
|
|
81
|
+
row: "prefs",
|
|
82
|
+
cell: "active_tab",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it("reports a cell with no control to read, and sends the rest", async () => {
|
|
86
|
+
const hub = await mountHub(`
|
|
87
|
+
<form lb-list="roster" lb-action="lb-row-insert">
|
|
88
|
+
<span lb-cell="label">not a control</span>
|
|
89
|
+
<input lb-cell="name" value="Bo" />
|
|
90
|
+
</form>`);
|
|
91
|
+
server.sent.length = 0;
|
|
92
|
+
const { warn } = await captured(() => submit(hub.querySelector("form")));
|
|
93
|
+
assert.match(warn.join("\n"), /lb-cell 'label' has no control to read/);
|
|
94
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
95
|
+
action: "lb-row-insert",
|
|
96
|
+
list: "roster",
|
|
97
|
+
values: { name: "Bo" },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it("refuses a reserved name it cannot turn into an operation", async () => {
|
|
101
|
+
// A cell change needs a widget to say what a change is, so a bare button
|
|
102
|
+
// naming one is a mistake the hub can see.
|
|
103
|
+
const hub = await mountHub(`
|
|
104
|
+
<ul lb-list="roster">
|
|
105
|
+
<li lb-key-value="42">
|
|
106
|
+
<button lb-action="lb-cell-change" lb-cell="name">go</button>
|
|
107
|
+
</li>
|
|
108
|
+
</ul>`);
|
|
109
|
+
server.sent.length = 0;
|
|
110
|
+
const { warn } = await captured(() => click(hub.querySelector("button")));
|
|
111
|
+
assert.match(warn.join("\n"), /cannot be sent from here/);
|
|
112
|
+
assert.equal(server.sent.length, 0);
|
|
113
|
+
});
|
|
114
|
+
it("refuses a delete with no key, since nothing says which row", async () => {
|
|
115
|
+
const hub = await mountHub(`
|
|
116
|
+
<ul lb-list="roster">
|
|
117
|
+
<button lb-action="lb-row-delete">x</button>
|
|
118
|
+
</ul>`);
|
|
119
|
+
server.sent.length = 0;
|
|
120
|
+
const { warn } = await captured(() => click(hub.querySelector("button")));
|
|
121
|
+
assert.match(warn.join("\n"), /cannot be sent from here/);
|
|
122
|
+
assert.equal(server.sent.length, 0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("which interactions the hub takes", () => {
|
|
126
|
+
it("finds the nearest lb-action above the element clicked", async () => {
|
|
127
|
+
const hub = await mountHub(`
|
|
128
|
+
<section lb-row="prefs">
|
|
129
|
+
<button lb-action="reset"><span>Reset</span></button>
|
|
130
|
+
</section>`);
|
|
131
|
+
server.sent.length = 0;
|
|
132
|
+
await click(hub.querySelector("span"));
|
|
133
|
+
assert.equal(server.sent[0]?.request.action, "reset");
|
|
134
|
+
});
|
|
135
|
+
it("leaves a widget's own click alone, since the widget owns it", async () => {
|
|
136
|
+
const hub = await mountHub(`<section lb-row="prefs"><lb-thing lb-action="reset"></lb-thing></section>`);
|
|
137
|
+
server.sent.length = 0;
|
|
138
|
+
await click(hub.querySelector("lb-thing"));
|
|
139
|
+
assert.equal(server.sent.length, 0);
|
|
140
|
+
});
|
|
141
|
+
it("sends a form's action once, on its submit rather than on the click", async () => {
|
|
142
|
+
// The button submits the form, so both listeners see the interaction.
|
|
143
|
+
// Only the submit turns into a request.
|
|
144
|
+
const hub = await mountHub(`
|
|
145
|
+
<form lb-list="roster" lb-action="lb-row-insert">
|
|
146
|
+
<input lb-cell="name" value="Bo" />
|
|
147
|
+
<button>Add</button>
|
|
148
|
+
</form>`);
|
|
149
|
+
server.sent.length = 0;
|
|
150
|
+
await click(hub.querySelector("button"));
|
|
151
|
+
assert.equal(server.sent.length, 1);
|
|
152
|
+
assert.deepEqual(server.sent[0]?.request, {
|
|
153
|
+
action: "lb-row-insert",
|
|
154
|
+
list: "roster",
|
|
155
|
+
values: { name: "Bo" },
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it("ignores an element carrying no action at all", async () => {
|
|
159
|
+
const hub = await mountHub(`<button>Plain</button>`);
|
|
160
|
+
server.sent.length = 0;
|
|
161
|
+
await click(hub.querySelector("button"));
|
|
162
|
+
assert.equal(server.sent.length, 0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe("what the hub refuses to forward", () => {
|
|
166
|
+
it("refuses an event with no action", async () => {
|
|
167
|
+
const hub = await mountHub(`<button>go</button>`);
|
|
168
|
+
server.sent.length = 0;
|
|
169
|
+
const { warn } = await captured(() => send(hub.querySelector("button"), undefined));
|
|
170
|
+
assert.match(warn.join("\n"), /event without an action/);
|
|
171
|
+
assert.equal(server.sent.length, 0);
|
|
172
|
+
});
|
|
173
|
+
it("refuses a reserved name that is not one of the four", async () => {
|
|
174
|
+
const hub = await mountHub(`<button>go</button>`);
|
|
175
|
+
server.sent.length = 0;
|
|
176
|
+
const { error } = await captured(() => send(hub.querySelector("button"), { action: "lb-row-rename" }));
|
|
177
|
+
assert.match(error.join("\n"), /names no operation/);
|
|
178
|
+
assert.equal(server.sent.length, 0);
|
|
179
|
+
});
|
|
180
|
+
it("forwards an operation a widget built, which the hub would not build itself", async () => {
|
|
181
|
+
const hub = await mountHub(`<lb-thing></lb-thing>`);
|
|
182
|
+
server.sent.length = 0;
|
|
183
|
+
await send(hub.querySelector("lb-thing"), {
|
|
184
|
+
action: "lb-cell-change",
|
|
185
|
+
list: "roster",
|
|
186
|
+
key: "42",
|
|
187
|
+
cell: "name",
|
|
188
|
+
value: "Ann",
|
|
189
|
+
});
|
|
190
|
+
assert.equal(server.sent[0]?.request.action, "lb-cell-change");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe("request state", () => {
|
|
194
|
+
it("stamps the element that dispatched, not the hub", async () => {
|
|
195
|
+
const hub = await mountHub(`<section lb-row="prefs"><button lb-action="reset">go</button></section>`);
|
|
196
|
+
const button = hub.querySelector("button");
|
|
197
|
+
let pendingWhileInFlight = false;
|
|
198
|
+
server.answer = {};
|
|
199
|
+
const watching = new Promise((resolve) => {
|
|
200
|
+
globalThis.queueMicrotask(() => {
|
|
201
|
+
pendingWhileInFlight = button.hasAttribute("lb-pending");
|
|
202
|
+
resolve();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
206
|
+
await watching;
|
|
207
|
+
await settle();
|
|
208
|
+
assert.equal(pendingWhileInFlight, true);
|
|
209
|
+
assert.equal(button.hasAttribute("lb-pending"), false);
|
|
210
|
+
assert.equal(hub.hasAttribute("lb-pending"), false);
|
|
211
|
+
});
|
|
212
|
+
it("sets lb-error on a failed round trip and clears it on the next", async () => {
|
|
213
|
+
const hub = await mountHub(`<section lb-row="prefs"><button lb-action="reset">go</button></section>`);
|
|
214
|
+
const button = hub.querySelector("button");
|
|
215
|
+
server.fail = "network down";
|
|
216
|
+
const { error } = await captured(() => click(button));
|
|
217
|
+
assert.match(error.join("\n"), /request failed/);
|
|
218
|
+
assert.equal(button.hasAttribute("lb-error"), true);
|
|
219
|
+
assert.equal(button.hasAttribute("lb-pending"), false);
|
|
220
|
+
delete server.fail;
|
|
221
|
+
await click(button);
|
|
222
|
+
assert.equal(button.hasAttribute("lb-error"), false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("finding a page host", () => {
|
|
226
|
+
it("lands the page the path names", async () => {
|
|
227
|
+
const hub = await mountHub(``, {
|
|
228
|
+
index: `<h1>Home</h1>`,
|
|
229
|
+
members: `<h1>Members</h1>`,
|
|
230
|
+
});
|
|
231
|
+
assert.match(hub.querySelector("main").innerHTML, /Home/);
|
|
232
|
+
});
|
|
233
|
+
it("reports a path with no host and opens the unknown-page dialog", async () => {
|
|
234
|
+
const { error } = await captured(() => mountHub(`<dialog lb-unknown-page></dialog>`, { index: `<h1>Home</h1>` }, "/nowhere"));
|
|
235
|
+
assert.match(error.join("\n"), /no page host for 'nowhere'/);
|
|
236
|
+
assert.equal(document.querySelector("dialog").hasAttribute("open"), true);
|
|
237
|
+
});
|
|
238
|
+
it("reports and carries on when a chrome declares no dialog", async () => {
|
|
239
|
+
const { error } = await captured(() => mountHub(``, { index: `<h1>Home</h1>` }, "/nowhere"));
|
|
240
|
+
assert.match(error.join("\n"), /no page host for 'nowhere'/);
|
|
241
|
+
});
|
|
242
|
+
it("reports two hosts for one name and takes the first", async () => {
|
|
243
|
+
// The builder refuses this — build/locations.ts — so reaching it means
|
|
244
|
+
// the document was assembled by something else.
|
|
245
|
+
const { error } = await captured(async () => {
|
|
246
|
+
history.replaceState(null, "", "/");
|
|
247
|
+
document.body.innerHTML =
|
|
248
|
+
`<lb-hub><main></main></lb-hub>` +
|
|
249
|
+
`<template lb-page="index"><h1>First</h1></template>` +
|
|
250
|
+
`<template lb-page="index"><h1>Second</h1></template>`;
|
|
251
|
+
await settle();
|
|
252
|
+
});
|
|
253
|
+
assert.match(error.join("\n"), /2 page hosts carry lb-page="index"/);
|
|
254
|
+
assert.match(document.querySelector("main").innerHTML, /First/);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe("navigation", () => {
|
|
258
|
+
it("swaps the host on an lb-nav-link and reverses it on popstate", async () => {
|
|
259
|
+
const hub = await mountHub(`<nav><a href="/members" lb-nav-link>Members</a></nav>`, { index: `<h1>Home</h1>`, members: `<h1>Members</h1>` });
|
|
260
|
+
const main = hub.querySelector("main");
|
|
261
|
+
assert.match(main.innerHTML, /Home/);
|
|
262
|
+
await click(hub.querySelector("a"));
|
|
263
|
+
assert.equal(location.pathname, "/members");
|
|
264
|
+
assert.match(main.innerHTML, /Members/);
|
|
265
|
+
await back();
|
|
266
|
+
assert.match(main.innerHTML, /Home/);
|
|
267
|
+
});
|
|
268
|
+
it("leaves an anchor without lb-nav-link to the browser", async () => {
|
|
269
|
+
const hub = await mountHub(`<nav><a href="/members">Members</a></nav>`, {
|
|
270
|
+
index: `<h1>Home</h1>`,
|
|
271
|
+
members: `<h1>Members</h1>`,
|
|
272
|
+
});
|
|
273
|
+
await click(hub.querySelector("a"));
|
|
274
|
+
assert.equal(location.pathname, "/");
|
|
275
|
+
assert.match(hub.querySelector("main").innerHTML, /Home/);
|
|
276
|
+
});
|
|
277
|
+
it("asks the server for the page the path names", async () => {
|
|
278
|
+
const hub = await mountHub(`<nav><a href="/members" lb-nav-link>Members</a></nav>`, { index: ``, members: `` });
|
|
279
|
+
server.sent.length = 0;
|
|
280
|
+
await click(hub.querySelector("a"));
|
|
281
|
+
assert.equal(server.sent[0]?.page, "members");
|
|
282
|
+
});
|
|
283
|
+
it("lands the navigation row with the path and the link that names it", async () => {
|
|
284
|
+
const hub = await mountHub(`<nav><a href="/members" lb-nav-link>Members</a></nav>
|
|
285
|
+
<p lb-row="lb-navigation">
|
|
286
|
+
<span lb-cell="page-label"></span> at <span lb-cell="page-uri"></span>
|
|
287
|
+
</p>`, { index: ``, members: `` });
|
|
288
|
+
await click(hub.querySelector("a"));
|
|
289
|
+
const row = hub.querySelector(`[lb-row="lb-navigation"]`);
|
|
290
|
+
assert.equal(row.querySelector(`[lb-cell="page-label"]`).textContent, "Members");
|
|
291
|
+
assert.equal(row.querySelector(`[lb-cell="page-uri"]`).textContent, "/members");
|
|
292
|
+
});
|
|
293
|
+
it("leaves the label empty when no link names the page", async () => {
|
|
294
|
+
const hub = await mountHub(`<p lb-row="lb-navigation"><span lb-cell="page-label"></span></p>`);
|
|
295
|
+
const label = hub.querySelector(`[lb-cell="page-label"]`);
|
|
296
|
+
assert.equal(label.textContent, "");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
describe("revealing the body", () => {
|
|
300
|
+
it("takes hidden off once a page has landed", async () => {
|
|
301
|
+
document.body.setAttribute("hidden", "");
|
|
302
|
+
await mountHub(``, { index: `<h1>Home</h1>` });
|
|
303
|
+
assert.equal(document.body.hasAttribute("hidden"), false);
|
|
304
|
+
});
|
|
305
|
+
it("takes hidden off even when the path names no page", async () => {
|
|
306
|
+
document.body.setAttribute("hidden", "");
|
|
307
|
+
await captured(() => mountHub(``, { index: `<h1>Home</h1>` }, "/nowhere"));
|
|
308
|
+
assert.equal(document.body.hasAttribute("hidden"), false);
|
|
309
|
+
});
|
|
310
|
+
it("takes hidden off even when the data fails to load", async () => {
|
|
311
|
+
document.body.setAttribute("hidden", "");
|
|
312
|
+
server.fail = "network down";
|
|
313
|
+
await captured(async () => {
|
|
314
|
+
await mountHub(``, { index: `<h1>Home</h1>` });
|
|
315
|
+
});
|
|
316
|
+
delete server.fail;
|
|
317
|
+
assert.equal(document.body.hasAttribute("hidden"), false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-list.test.d.ts","sourceRoot":"","sources":["../../tests/lb-list.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|