@loadbare/app 0.5.5 → 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.
Files changed (81) hide show
  1. package/README.md +3 -4
  2. package/dist/build/assemble.d.ts +1 -1
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +80 -7
  5. package/dist/build/cli.d.ts +2 -2
  6. package/dist/build/cli.js +2 -2
  7. package/dist/build/expand.d.ts.map +1 -1
  8. package/dist/build/expand.js +30 -34
  9. package/dist/build/locations.d.ts +3 -3
  10. package/dist/build/locations.d.ts.map +1 -1
  11. package/dist/build/locations.js +15 -3
  12. package/dist/build/origins.d.ts +0 -13
  13. package/dist/build/origins.d.ts.map +1 -1
  14. package/dist/build/origins.js +32 -8
  15. package/dist/build/pages.d.ts +3 -3
  16. package/dist/build/pages.d.ts.map +1 -1
  17. package/dist/build/pages.js +9 -7
  18. package/dist/core/lb-constants.d.ts +17 -12
  19. package/dist/core/lb-constants.d.ts.map +1 -1
  20. package/dist/core/lb-constants.js +106 -43
  21. package/dist/core/lb-types.d.ts +103 -62
  22. package/dist/core/lb-types.d.ts.map +1 -1
  23. package/dist/core/lb-types.js +11 -3
  24. package/dist/hub/lb-apply.d.ts +18 -4
  25. package/dist/hub/lb-apply.d.ts.map +1 -1
  26. package/dist/hub/lb-apply.js +184 -34
  27. package/dist/hub/lb-hub.browser.d.ts +1 -1
  28. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  29. package/dist/hub/lb-hub.browser.js +165 -83
  30. package/dist/server/lb-express.d.ts +7 -4
  31. package/dist/server/lb-express.d.ts.map +1 -1
  32. package/dist/server/lb-express.js +45 -33
  33. package/dist/server/lb-server.d.ts +67 -45
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +55 -18
  36. package/dist/tests/assemble.test.js +154 -2
  37. package/dist/tests/expand.test.js +30 -3
  38. package/dist/tests/helpers/hub.d.ts +73 -0
  39. package/dist/tests/helpers/hub.d.ts.map +1 -0
  40. package/dist/tests/helpers/hub.js +151 -0
  41. package/dist/tests/lb-apply.test.js +86 -62
  42. package/dist/tests/lb-express.test.d.ts +1 -1
  43. package/dist/tests/lb-express.test.js +43 -38
  44. package/dist/tests/lb-hub.test.d.ts +14 -0
  45. package/dist/tests/lb-hub.test.d.ts.map +1 -0
  46. package/dist/tests/lb-hub.test.js +319 -0
  47. package/dist/tests/{lb-rows.test.d.ts → lb-list.test.d.ts} +1 -1
  48. package/dist/tests/lb-list.test.d.ts.map +1 -0
  49. package/dist/tests/{lb-rows.test.js → lb-list.test.js} +109 -106
  50. package/dist/tests/lb-server.test.js +151 -100
  51. package/dist/tests/origins.test.js +19 -1
  52. package/dist/tests/pages.test.d.ts +1 -1
  53. package/dist/tests/pages.test.js +64 -14
  54. package/docs/TECHREF-1.0.md +1000 -0
  55. package/docs/reference/builder.md +4 -4
  56. package/docs/reference/chrome.md +56 -5
  57. package/docs/reference/custom-elements.md +59 -36
  58. package/docs/reference/data-binding.md +142 -84
  59. package/docs/reference/overview.md +1 -1
  60. package/docs/reference/page-files.md +64 -49
  61. package/docs/reference/server.md +7 -6
  62. package/docs/reference/widgets.md +43 -32
  63. package/docs/roadmap.md +68 -22
  64. package/docs/testing.md +47 -17
  65. package/docs/theory.md +2 -2
  66. package/docs/tutorials/010-pages-and-navigation.md +14 -8
  67. package/docs/tutorials/040-displaying-data.md +9 -9
  68. package/docs/tutorials/050-actions.md +5 -5
  69. package/docs/tutorials/060-custom-element-code.md +1 -1
  70. package/docs/tutorials/065-conditional-rendering.md +4 -4
  71. package/docs/tutorials/070-displaying-a-list.md +24 -47
  72. package/docs/tutorials/072-inserting-into-a-list.md +17 -17
  73. package/docs/tutorials/074-deleting-from-a-list.md +15 -17
  74. package/docs/tutorials/076-updating-a-list-item.md +20 -22
  75. package/docs/tutorials/080-widget-requests.md +27 -23
  76. package/docs/tutorials/090-using-widget-libraries.md +23 -1
  77. package/package.json +1 -2
  78. package/dist/hub/lb-rows.d.ts +0 -18
  79. package/dist/hub/lb-rows.d.ts.map +0 -1
  80. package/dist/hub/lb-rows.js +0 -106
  81. 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: two endpoints and no third, the page from the
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 { LB_DATA_ENDPOINT, LB_REQUEST_ENDPOINT } from "../core/lb-constants";
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 runTupleDelete(page, where, ctx) {
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 runTupleInsert(page, where, ctx) {
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 runTupleUpdate(page, where, ctx) {
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}${LB_REQUEST_ENDPOINT}?page=${page}`, {
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("the data endpoint", () => {
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 fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
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}${LB_DATA_ENDPOINT}`);
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("the request endpoint", () => {
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
- op: "action",
93
- name: "pick",
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
- query: "members",
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", { op: "action", name: "bump" });
118
+ await post("counter", { action: "bump" });
115
119
  assert.deepEqual(calls[0].where, {
116
- query: undefined,
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 an op it does not recognise, rather than guessing", async () => {
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", { op: "tuple-upsert", query: "members", key: "1" }));
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 'tuple-upsert' not implemented/);
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
- op: "cell-change",
134
- query: "members",
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
- query: "members",
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
- op: "tuple-delete",
154
- query: "members",
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, { query: "members", key: "7" });
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
- op: "tuple-insert",
167
- query: "members",
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
- query: "members",
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
- op: "tuple-update",
183
- query: "members",
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
- query: "members",
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", { op: "action", name: "explode", query: "members" }));
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"), /operation 'action' threw/);
213
+ assert.match(error.join("\n"), /'explode' threw/);
209
214
  // And the process is still serving.
210
- const next = await fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
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 fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
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 fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
232
- await fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
233
- await post("counter", { op: "action", name: "bump" });
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
+ });
@@ -9,4 +9,4 @@
9
9
  * convergence, and two properties state it better than twenty examples.
10
10
  */
11
11
  export {};
12
- //# sourceMappingURL=lb-rows.test.d.ts.map
12
+ //# sourceMappingURL=lb-list.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-list.test.d.ts","sourceRoot":"","sources":["../../tests/lb-list.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}