@loadbare/app 0.6.0 → 0.7.1
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/dist/build/assemble.js +1 -0
- package/dist/build/assemble.js.map +1 -0
- package/dist/build/cli.js +1 -0
- package/dist/build/cli.js.map +1 -0
- package/dist/build/elements.js +1 -0
- package/dist/build/elements.js.map +1 -0
- package/dist/build/expand.js +1 -0
- package/dist/build/expand.js.map +1 -0
- package/dist/build/format.js +1 -0
- package/dist/build/format.js.map +1 -0
- package/dist/build/locations.d.ts +2 -2
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +3 -4
- package/dist/build/locations.js.map +1 -0
- package/dist/build/origins.js +1 -0
- package/dist/build/origins.js.map +1 -0
- package/dist/build/package-root.js +1 -0
- package/dist/build/package-root.js.map +1 -0
- package/dist/build/pages.d.ts +4 -0
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +5 -0
- package/dist/build/pages.js.map +1 -0
- package/dist/build/styles.js +1 -0
- package/dist/build/styles.js.map +1 -0
- package/dist/core/lb-constants.js +1 -0
- package/dist/core/lb-constants.js.map +1 -0
- package/dist/core/lb-types.d.ts +1 -1
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +2 -1
- package/dist/core/lb-types.js.map +1 -0
- package/dist/hub/lb-apply.d.ts +11 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +50 -13
- package/dist/hub/lb-apply.js.map +1 -0
- 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 +143 -112
- package/dist/hub/lb-hub.browser.js.map +1 -0
- package/dist/server/lb-express.d.ts +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +1 -0
- package/dist/server/lb-express.js.map +1 -0
- package/dist/server/lb-server.d.ts +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +2 -1
- package/dist/server/lb-server.js.map +1 -0
- package/docs/TECHREF-1.0.md +158 -43
- package/docs/analysis-accidental-complexity.md +149 -0
- package/docs/reference/custom-elements.md +27 -17
- package/docs/reference/data-binding.md +88 -29
- package/docs/reference/server.md +18 -0
- package/docs/reference/widgets.md +3 -3
- package/docs/theory.md +114 -1
- package/docs/tutorials/072-inserting-into-a-list.md +14 -11
- package/docs/tutorials/080-widget-requests.md +9 -26
- package/package.json +2 -2
- package/dist/tests/assemble.test.d.ts +0 -8
- package/dist/tests/assemble.test.d.ts.map +0 -1
- package/dist/tests/assemble.test.js +0 -210
- package/dist/tests/elements.test.d.ts +0 -8
- package/dist/tests/elements.test.d.ts.map +0 -1
- package/dist/tests/elements.test.js +0 -118
- package/dist/tests/expand.test.d.ts +0 -10
- package/dist/tests/expand.test.d.ts.map +0 -1
- package/dist/tests/expand.test.js +0 -253
- package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/imports.js +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
- package/dist/tests/helpers/console.d.ts +0 -20
- package/dist/tests/helpers/console.d.ts.map +0 -1
- package/dist/tests/helpers/console.js +0 -28
- package/dist/tests/helpers/dom.d.ts +0 -18
- package/dist/tests/helpers/dom.d.ts.map +0 -1
- package/dist/tests/helpers/dom.js +0 -22
- package/dist/tests/helpers/hub.d.ts +0 -73
- package/dist/tests/helpers/hub.d.ts.map +0 -1
- package/dist/tests/helpers/hub.js +0 -151
- package/dist/tests/lb-apply.test.d.ts +0 -8
- package/dist/tests/lb-apply.test.d.ts.map +0 -1
- package/dist/tests/lb-apply.test.js +0 -177
- package/dist/tests/lb-express.test.d.ts +0 -14
- package/dist/tests/lb-express.test.d.ts.map +0 -1
- package/dist/tests/lb-express.test.js +0 -243
- package/dist/tests/lb-hub.test.d.ts +0 -14
- package/dist/tests/lb-hub.test.d.ts.map +0 -1
- package/dist/tests/lb-hub.test.js +0 -319
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -339
- package/dist/tests/lb-server.test.d.ts +0 -9
- package/dist/tests/lb-server.test.d.ts.map +0 -1
- package/dist/tests/lb-server.test.js +0 -546
- package/dist/tests/origins.test.d.ts +0 -10
- package/dist/tests/origins.test.d.ts.map +0 -1
- package/dist/tests/origins.test.js +0 -387
- package/dist/tests/pages.test.d.ts +0 -6
- package/dist/tests/pages.test.d.ts.map +0 -1
- package/dist/tests/pages.test.js +0 -148
- package/dist/tests/styles.test.d.ts +0 -7
- package/dist/tests/styles.test.d.ts.map +0 -1
- package/dist/tests/styles.test.js +0 -76
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Express adapter — see docs/testing.md, tier 2.
|
|
3
|
-
*
|
|
4
|
-
* server/lb-express.ts is the only file that knows both HTTP and Hub. What
|
|
5
|
-
* it owes is small and exact: one endpoint and no second, the page from the
|
|
6
|
-
* query string, and an operation it does not yet handle refused rather than
|
|
7
|
-
* half-handled.
|
|
8
|
-
*
|
|
9
|
-
* It is tested against a listening server rather than a mocked request,
|
|
10
|
-
* because the things it can get wrong — a body that never parsed, a route
|
|
11
|
-
* that swallows another — are things a mock would agree with.
|
|
12
|
-
*/
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { after, before, describe, it } from "node:test";
|
|
15
|
-
import express from "express";
|
|
16
|
-
import { LB_ENDPOINT } from "../core/lb-constants";
|
|
17
|
-
import { hubRoutes } from "../server/lb-express";
|
|
18
|
-
import { captured } from "./helpers/console";
|
|
19
|
-
const calls = [];
|
|
20
|
-
let contexts = 0;
|
|
21
|
-
const engine = {
|
|
22
|
-
async dataForPage(page, ctx) {
|
|
23
|
-
calls.push({ kind: "data", page, ctx });
|
|
24
|
-
return { greeting: { text: "hello" } };
|
|
25
|
-
},
|
|
26
|
-
async runAction(page, name, where, ctx) {
|
|
27
|
-
calls.push({ kind: "action", page, name, where, ctx });
|
|
28
|
-
if (name === "explode")
|
|
29
|
-
throw new Error("application code failed");
|
|
30
|
-
return { count: { n: "2" } };
|
|
31
|
-
},
|
|
32
|
-
async runCellChange(page, where, ctx) {
|
|
33
|
-
calls.push({ kind: "cell-change", page, where, ctx });
|
|
34
|
-
return { members: { n: "3" } };
|
|
35
|
-
},
|
|
36
|
-
async runRowDelete(page, where, ctx) {
|
|
37
|
-
calls.push({ kind: "tuple-delete", page, where, ctx });
|
|
38
|
-
return { members: { n: "4" } };
|
|
39
|
-
},
|
|
40
|
-
async runRowInsert(page, where, ctx) {
|
|
41
|
-
calls.push({ kind: "tuple-insert", page, where, ctx });
|
|
42
|
-
return { members: { n: "5" } };
|
|
43
|
-
},
|
|
44
|
-
async runRowUpdate(page, where, ctx) {
|
|
45
|
-
calls.push({ kind: "tuple-update", page, where, ctx });
|
|
46
|
-
return { members: { n: "6" } };
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
let base;
|
|
50
|
-
let server;
|
|
51
|
-
before(async () => {
|
|
52
|
-
const app = express();
|
|
53
|
-
app.use(hubRoutes(engine, () => {
|
|
54
|
-
contexts += 1;
|
|
55
|
-
return {};
|
|
56
|
-
}));
|
|
57
|
-
await new Promise((done) => {
|
|
58
|
-
server = app.listen(0, "127.0.0.1", () => done());
|
|
59
|
-
});
|
|
60
|
-
base = `http://127.0.0.1:${server.address().port}`;
|
|
61
|
-
});
|
|
62
|
-
after(() => void server.close());
|
|
63
|
-
function post(page, body, raw) {
|
|
64
|
-
return fetch(`${base}${LB_ENDPOINT}?page=${page}`, {
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: { "content-type": "application/json" },
|
|
67
|
-
body: raw ?? JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
describe("a request with no action", () => {
|
|
71
|
-
it("runs the page named in the query string", async () => {
|
|
72
|
-
calls.length = 0;
|
|
73
|
-
const res = await post("hello", {});
|
|
74
|
-
assert.equal(res.status, 200);
|
|
75
|
-
assert.deepEqual(await res.json(), { greeting: { text: "hello" } });
|
|
76
|
-
assert.equal(calls.length, 1);
|
|
77
|
-
assert.equal(calls[0].kind, "data");
|
|
78
|
-
assert.equal(calls[0].page, "hello");
|
|
79
|
-
});
|
|
80
|
-
it("names the empty page when the query string does not", async () => {
|
|
81
|
-
// The engine answers for a page it does not have; the route's job is only
|
|
82
|
-
// to pass on what it was given rather than to invent a default.
|
|
83
|
-
calls.length = 0;
|
|
84
|
-
await fetch(`${base}${LB_ENDPOINT}`, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: { "content-type": "application/json" },
|
|
87
|
-
body: "{}",
|
|
88
|
-
});
|
|
89
|
-
assert.equal(calls[0].page, "");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe("a request carrying an action", () => {
|
|
93
|
-
it("runs the action, with where the interaction happened", async () => {
|
|
94
|
-
calls.length = 0;
|
|
95
|
-
const res = await post("roster", {
|
|
96
|
-
action: "pick",
|
|
97
|
-
list: "members",
|
|
98
|
-
key: "7",
|
|
99
|
-
cell: "role",
|
|
100
|
-
value: "lead",
|
|
101
|
-
});
|
|
102
|
-
assert.equal(res.status, 200);
|
|
103
|
-
assert.deepEqual(await res.json(), { count: { n: "2" } });
|
|
104
|
-
assert.deepEqual(calls[0].where, {
|
|
105
|
-
list: "members",
|
|
106
|
-
row: undefined,
|
|
107
|
-
key: "7",
|
|
108
|
-
cell: "role",
|
|
109
|
-
value: "lead",
|
|
110
|
-
});
|
|
111
|
-
assert.equal(calls[0].name, "pick");
|
|
112
|
-
assert.equal(calls[0].page, "roster");
|
|
113
|
-
});
|
|
114
|
-
it("passes on an action carrying no binding at all", async () => {
|
|
115
|
-
// A button carries no value and may sit outside any scope. Undefined
|
|
116
|
-
// stays undefined rather than becoming the empty string.
|
|
117
|
-
calls.length = 0;
|
|
118
|
-
await post("counter", { action: "bump" });
|
|
119
|
-
assert.deepEqual(calls[0].where, {
|
|
120
|
-
list: undefined,
|
|
121
|
-
row: undefined,
|
|
122
|
-
key: undefined,
|
|
123
|
-
cell: undefined,
|
|
124
|
-
value: undefined,
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
it("refuses a reserved name it does not implement, rather than guessing", async () => {
|
|
128
|
-
calls.length = 0;
|
|
129
|
-
const { result: res, warn } = await captured(() => post("roster", { action: "lb-row-upsert", list: "members", key: "1" }));
|
|
130
|
-
assert.equal(res.status, 400);
|
|
131
|
-
assert.deepEqual(await res.json(), {});
|
|
132
|
-
assert.deepEqual(calls, [], "the engine was never reached");
|
|
133
|
-
assert.match(warn.join("\n"), /operation 'lb-row-upsert' not implemented/);
|
|
134
|
-
});
|
|
135
|
-
it("runs a cell-change, with where the interaction happened", async () => {
|
|
136
|
-
calls.length = 0;
|
|
137
|
-
const res = await post("roster", {
|
|
138
|
-
action: "lb-cell-change",
|
|
139
|
-
list: "members",
|
|
140
|
-
key: "7",
|
|
141
|
-
cell: "name",
|
|
142
|
-
value: "Ada",
|
|
143
|
-
});
|
|
144
|
-
assert.equal(res.status, 200);
|
|
145
|
-
assert.deepEqual(await res.json(), { members: { n: "3" } });
|
|
146
|
-
assert.deepEqual(calls[0].where, {
|
|
147
|
-
list: "members",
|
|
148
|
-
key: "7",
|
|
149
|
-
cell: "name",
|
|
150
|
-
value: "Ada",
|
|
151
|
-
});
|
|
152
|
-
assert.equal(calls[0].kind, "cell-change");
|
|
153
|
-
assert.equal(calls[0].page, "roster");
|
|
154
|
-
});
|
|
155
|
-
it("runs a tuple-delete, with the key of the row", async () => {
|
|
156
|
-
calls.length = 0;
|
|
157
|
-
const res = await post("roster", {
|
|
158
|
-
action: "lb-row-delete",
|
|
159
|
-
list: "members",
|
|
160
|
-
key: "7",
|
|
161
|
-
});
|
|
162
|
-
assert.equal(res.status, 200);
|
|
163
|
-
assert.deepEqual(await res.json(), { members: { n: "4" } });
|
|
164
|
-
assert.deepEqual(calls[0].where, { list: "members", key: "7" });
|
|
165
|
-
assert.equal(calls[0].kind, "tuple-delete");
|
|
166
|
-
assert.equal(calls[0].page, "roster");
|
|
167
|
-
});
|
|
168
|
-
it("runs a tuple-insert, with the gathered values", async () => {
|
|
169
|
-
calls.length = 0;
|
|
170
|
-
const res = await post("roster", {
|
|
171
|
-
action: "lb-row-insert",
|
|
172
|
-
list: "members",
|
|
173
|
-
values: { name: "Ada", role: "Analyst" },
|
|
174
|
-
});
|
|
175
|
-
assert.equal(res.status, 200);
|
|
176
|
-
assert.deepEqual(await res.json(), { members: { n: "5" } });
|
|
177
|
-
assert.deepEqual(calls[0].where, {
|
|
178
|
-
list: "members",
|
|
179
|
-
values: { name: "Ada", role: "Analyst" },
|
|
180
|
-
});
|
|
181
|
-
assert.equal(calls[0].kind, "tuple-insert");
|
|
182
|
-
assert.equal(calls[0].page, "roster");
|
|
183
|
-
});
|
|
184
|
-
it("runs a tuple-update, with the key and the gathered values", async () => {
|
|
185
|
-
calls.length = 0;
|
|
186
|
-
const res = await post("roster", {
|
|
187
|
-
action: "lb-row-update",
|
|
188
|
-
list: "members",
|
|
189
|
-
key: "7",
|
|
190
|
-
values: { name: "Ada", role: "Analyst" },
|
|
191
|
-
});
|
|
192
|
-
assert.equal(res.status, 200);
|
|
193
|
-
assert.deepEqual(await res.json(), { members: { n: "6" } });
|
|
194
|
-
assert.deepEqual(calls[0].where, {
|
|
195
|
-
list: "members",
|
|
196
|
-
key: "7",
|
|
197
|
-
values: { name: "Ada", role: "Analyst" },
|
|
198
|
-
});
|
|
199
|
-
assert.equal(calls[0].kind, "tuple-update");
|
|
200
|
-
assert.equal(calls[0].page, "roster");
|
|
201
|
-
});
|
|
202
|
-
it("refuses a body with no op", async () => {
|
|
203
|
-
calls.length = 0;
|
|
204
|
-
const { result: res } = await captured(() => post("roster", { name: "pick" }));
|
|
205
|
-
assert.equal(res.status, 400);
|
|
206
|
-
assert.deepEqual(calls, []);
|
|
207
|
-
});
|
|
208
|
-
it("survives an operation that throws, rather than crashing", async () => {
|
|
209
|
-
calls.length = 0;
|
|
210
|
-
const { result: res, error } = await captured(() => post("roster", { action: "explode", list: "members" }));
|
|
211
|
-
assert.equal(res.status, 500);
|
|
212
|
-
assert.deepEqual(await res.json(), {});
|
|
213
|
-
assert.match(error.join("\n"), /'explode' threw/);
|
|
214
|
-
// And the process is still serving.
|
|
215
|
-
const next = await post("hello", {});
|
|
216
|
-
assert.equal(next.status, 200);
|
|
217
|
-
});
|
|
218
|
-
it("survives a body that is not JSON", async () => {
|
|
219
|
-
calls.length = 0;
|
|
220
|
-
const res = await post("roster", null, "{ not json");
|
|
221
|
-
assert.ok(res.status >= 400, `expected a refusal, got ${res.status}`);
|
|
222
|
-
assert.deepEqual(calls, []);
|
|
223
|
-
// And the process is still serving. A parse failure is one request's
|
|
224
|
-
// problem, not the server's.
|
|
225
|
-
const next = await post("hello", {});
|
|
226
|
-
assert.equal(next.status, 200);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("the context", () => {
|
|
230
|
-
it("is built once per request, by the application", async () => {
|
|
231
|
-
// Hub's only requirement is that the context is built per request,
|
|
232
|
-
// after whatever establishes identity. Holding one for the life of the
|
|
233
|
-
// process is the mistake this asserts against.
|
|
234
|
-
contexts = 0;
|
|
235
|
-
calls.length = 0;
|
|
236
|
-
await post("hello", {});
|
|
237
|
-
await post("hello", {});
|
|
238
|
-
await post("counter", { action: "bump" });
|
|
239
|
-
assert.equal(contexts, 3);
|
|
240
|
-
assert.notEqual(calls[0].ctx, calls[1].ctx);
|
|
241
|
-
assert.notEqual(calls[1].ctx, calls[2].ctx);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lb-hub.test.d.ts","sourceRoot":"","sources":["../../tests/lb-hub.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -1,319 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The row machinery — see docs/testing.md, tier 3.
|
|
3
|
-
*
|
|
4
|
-
* This is the most intricate code in the framework and the most likely to
|
|
5
|
-
* break in ways nobody notices, because what it gets wrong is a row in the
|
|
6
|
-
* wrong place rather than an exception.
|
|
7
|
-
*
|
|
8
|
-
* The last describe holds the two properties. Reconciliation's contract is
|
|
9
|
-
* convergence, and two properties state it better than twenty examples.
|
|
10
|
-
*/
|
|
11
|
-
export {};
|
|
12
|
-
//# sourceMappingURL=lb-list.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lb-list.test.d.ts","sourceRoot":"","sources":["../../tests/lb-list.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|