@loadbare/app 0.4.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 (171) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +95 -0
  3. package/dist/build/assemble.d.ts +31 -0
  4. package/dist/build/assemble.d.ts.map +1 -0
  5. package/dist/build/assemble.js +53 -0
  6. package/dist/build/cli.d.ts +30 -0
  7. package/dist/build/cli.d.ts.map +1 -0
  8. package/dist/build/cli.js +107 -0
  9. package/dist/build/elements.d.ts +61 -0
  10. package/dist/build/elements.d.ts.map +1 -0
  11. package/dist/build/elements.js +158 -0
  12. package/dist/build/expand.d.ts +45 -0
  13. package/dist/build/expand.d.ts.map +1 -0
  14. package/dist/build/expand.js +386 -0
  15. package/dist/build/format.d.ts +28 -0
  16. package/dist/build/format.d.ts.map +1 -0
  17. package/dist/build/format.js +42 -0
  18. package/dist/build/locations.d.ts +87 -0
  19. package/dist/build/locations.d.ts.map +1 -0
  20. package/dist/build/locations.js +173 -0
  21. package/dist/build/package-root.d.ts +9 -0
  22. package/dist/build/package-root.d.ts.map +1 -0
  23. package/dist/build/package-root.js +24 -0
  24. package/dist/build/pages.d.ts +25 -0
  25. package/dist/build/pages.d.ts.map +1 -0
  26. package/dist/build/pages.js +54 -0
  27. package/dist/build/styles.d.ts +13 -0
  28. package/dist/build/styles.d.ts.map +1 -0
  29. package/dist/build/styles.js +18 -0
  30. package/dist/client.js +522 -0
  31. package/dist/core/lb-constants.d.ts +23 -0
  32. package/dist/core/lb-constants.d.ts.map +1 -0
  33. package/dist/core/lb-constants.js +95 -0
  34. package/dist/core/lb-types.d.ts +88 -0
  35. package/dist/core/lb-types.d.ts.map +1 -0
  36. package/dist/core/lb-types.js +5 -0
  37. package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
  38. package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
  39. package/dist/demo-static/src/widgets/app-box.js +19 -0
  40. package/dist/hub/lb-apply.d.ts +13 -0
  41. package/dist/hub/lb-apply.d.ts.map +1 -0
  42. package/dist/hub/lb-apply.js +77 -0
  43. package/dist/hub/lb-hub.d.ts +2 -0
  44. package/dist/hub/lb-hub.d.ts.map +1 -0
  45. package/dist/hub/lb-hub.js +242 -0
  46. package/dist/hub/lb-rows.d.ts +18 -0
  47. package/dist/hub/lb-rows.d.ts.map +1 -0
  48. package/dist/hub/lb-rows.js +106 -0
  49. package/dist/server/lb-express.d.ts +28 -0
  50. package/dist/server/lb-express.d.ts.map +1 -0
  51. package/dist/server/lb-express.js +77 -0
  52. package/dist/server/lb-server.d.ts +174 -0
  53. package/dist/server/lb-server.d.ts.map +1 -0
  54. package/dist/server/lb-server.js +79 -0
  55. package/dist/tests/assemble.test.d.ts +8 -0
  56. package/dist/tests/assemble.test.d.ts.map +1 -0
  57. package/dist/tests/assemble.test.js +51 -0
  58. package/dist/tests/elements.test.d.ts +8 -0
  59. package/dist/tests/elements.test.d.ts.map +1 -0
  60. package/dist/tests/elements.test.js +111 -0
  61. package/dist/tests/expand.test.d.ts +10 -0
  62. package/dist/tests/expand.test.d.ts.map +1 -0
  63. package/dist/tests/expand.test.js +226 -0
  64. package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
  65. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
  66. package/dist/tests/fixtures/elements/collision/elements.js +3 -0
  67. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
  68. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
  69. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
  70. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
  71. package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
  72. package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
  73. package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
  74. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
  75. package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
  76. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
  77. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
  78. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
  79. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
  80. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
  81. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
  82. package/dist/tests/golden.test.d.ts +19 -0
  83. package/dist/tests/golden.test.d.ts.map +1 -0
  84. package/dist/tests/golden.test.js +60 -0
  85. package/dist/tests/helpers/console.d.ts +20 -0
  86. package/dist/tests/helpers/console.d.ts.map +1 -0
  87. package/dist/tests/helpers/console.js +28 -0
  88. package/dist/tests/helpers/dom.d.ts +18 -0
  89. package/dist/tests/helpers/dom.d.ts.map +1 -0
  90. package/dist/tests/helpers/dom.js +22 -0
  91. package/dist/tests/helpers/window.d.ts +43 -0
  92. package/dist/tests/helpers/window.d.ts.map +1 -0
  93. package/dist/tests/helpers/window.js +78 -0
  94. package/dist/tests/lb-apply.test.d.ts +8 -0
  95. package/dist/tests/lb-apply.test.d.ts.map +1 -0
  96. package/dist/tests/lb-apply.test.js +153 -0
  97. package/dist/tests/lb-express.test.d.ts +14 -0
  98. package/dist/tests/lb-express.test.d.ts.map +1 -0
  99. package/dist/tests/lb-express.test.js +238 -0
  100. package/dist/tests/lb-input.test.d.ts +9 -0
  101. package/dist/tests/lb-input.test.d.ts.map +1 -0
  102. package/dist/tests/lb-input.test.js +78 -0
  103. package/dist/tests/lb-list.test.d.ts +12 -0
  104. package/dist/tests/lb-list.test.d.ts.map +1 -0
  105. package/dist/tests/lb-list.test.js +44 -0
  106. package/dist/tests/lb-options.test.d.ts +10 -0
  107. package/dist/tests/lb-options.test.d.ts.map +1 -0
  108. package/dist/tests/lb-options.test.js +121 -0
  109. package/dist/tests/lb-picker.test.d.ts +14 -0
  110. package/dist/tests/lb-picker.test.d.ts.map +1 -0
  111. package/dist/tests/lb-picker.test.js +59 -0
  112. package/dist/tests/lb-rows.test.d.ts +12 -0
  113. package/dist/tests/lb-rows.test.d.ts.map +1 -0
  114. package/dist/tests/lb-rows.test.js +336 -0
  115. package/dist/tests/lb-select.test.d.ts +9 -0
  116. package/dist/tests/lb-select.test.d.ts.map +1 -0
  117. package/dist/tests/lb-select.test.js +71 -0
  118. package/dist/tests/lb-server.test.d.ts +9 -0
  119. package/dist/tests/lb-server.test.d.ts.map +1 -0
  120. package/dist/tests/lb-server.test.js +495 -0
  121. package/dist/tests/lb-table.test.d.ts +15 -0
  122. package/dist/tests/lb-table.test.d.ts.map +1 -0
  123. package/dist/tests/lb-table.test.js +205 -0
  124. package/dist/tests/pages.test.d.ts +6 -0
  125. package/dist/tests/pages.test.d.ts.map +1 -0
  126. package/dist/tests/pages.test.js +98 -0
  127. package/dist/tests/styles.test.d.ts +7 -0
  128. package/dist/tests/styles.test.d.ts.map +1 -0
  129. package/dist/tests/styles.test.js +73 -0
  130. package/dist/widgets/index.d.ts +7 -0
  131. package/dist/widgets/index.d.ts.map +1 -0
  132. package/dist/widgets/index.js +6 -0
  133. package/dist/widgets/lb-input.d.ts +2 -0
  134. package/dist/widgets/lb-input.d.ts.map +1 -0
  135. package/dist/widgets/lb-input.js +48 -0
  136. package/dist/widgets/lb-list.d.ts +2 -0
  137. package/dist/widgets/lb-list.d.ts.map +1 -0
  138. package/dist/widgets/lb-list.js +17 -0
  139. package/dist/widgets/lb-options.d.ts +26 -0
  140. package/dist/widgets/lb-options.d.ts.map +1 -0
  141. package/dist/widgets/lb-options.js +72 -0
  142. package/dist/widgets/lb-picker.d.ts +2 -0
  143. package/dist/widgets/lb-picker.d.ts.map +1 -0
  144. package/dist/widgets/lb-picker.js +25 -0
  145. package/dist/widgets/lb-select.d.ts +2 -0
  146. package/dist/widgets/lb-select.d.ts.map +1 -0
  147. package/dist/widgets/lb-select.js +43 -0
  148. package/dist/widgets/lb-table.d.ts +2 -0
  149. package/dist/widgets/lb-table.d.ts.map +1 -0
  150. package/dist/widgets/lb-table.js +113 -0
  151. package/docs/application-chrome.md +36 -0
  152. package/docs/building-html-pages.md +130 -0
  153. package/docs/getting-started.md +120 -0
  154. package/docs/guide.md +1164 -0
  155. package/docs/hosting.md +218 -0
  156. package/docs/latent-risks.md +20 -0
  157. package/docs/theory.md +226 -0
  158. package/package.json +85 -0
  159. package/widgets/index.ts +6 -0
  160. package/widgets/lb-input.html +1 -0
  161. package/widgets/lb-input.ts +64 -0
  162. package/widgets/lb-list.html +1 -0
  163. package/widgets/lb-list.ts +21 -0
  164. package/widgets/lb-options.html +4 -0
  165. package/widgets/lb-options.ts +88 -0
  166. package/widgets/lb-picker.html +7 -0
  167. package/widgets/lb-picker.ts +27 -0
  168. package/widgets/lb-select.html +4 -0
  169. package/widgets/lb-select.ts +55 -0
  170. package/widgets/lb-table.html +8 -0
  171. package/widgets/lb-table.ts +126 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * The engine — see docs/testing.md, tier 2.
3
+ *
4
+ * `createHub` takes a plain object and returns an object. Nothing in
5
+ * server/lb-server.ts opens a socket or knows what a request is, so every
6
+ * fixture here is a counting stub and the whole tier runs in node.
7
+ */
8
+ import assert from "node:assert/strict";
9
+ import { describe, it } from "node:test";
10
+ import { createHub, patch, rows, } from "../server/lb-server";
11
+ import { isProjection } from "../core/lb-types";
12
+ import { captured } from "./helpers/console";
13
+ /**
14
+ * A context to compare by identity.
15
+ *
16
+ * The engine never reads the context — it only hands it on — so what a test
17
+ * needs of one is that it can tell the same object apart from another. The
18
+ * cast is because an application fills the interface in by declaration
19
+ * merging, and the demo's is the one in this compilation.
20
+ */
21
+ function context() {
22
+ return {};
23
+ }
24
+ /** A query that answers a fixed tuple and records that it ran. */
25
+ function stub(log, name, answer) {
26
+ return (ctx) => {
27
+ log.push(name);
28
+ seen.set(name, ctx);
29
+ return answer;
30
+ };
31
+ }
32
+ const seen = new Map();
33
+ function pages(entries) {
34
+ const map = {};
35
+ for (const [name, entry] of Object.entries(entries)) {
36
+ map[name] = { queries: entry.queries, hooks: entry.hooks ?? {} };
37
+ }
38
+ return map;
39
+ }
40
+ // --- The page GET --------------------------------------------------------
41
+ describe("a page GET", () => {
42
+ it("runs beforeGet before any query, then the whole query set", async () => {
43
+ const log = [];
44
+ const hub = createHub(pages({
45
+ report: {
46
+ queries: {
47
+ head: stub(log, "head", { title: "Q1" }),
48
+ body: stub(log, "body", { line: "one" }),
49
+ },
50
+ hooks: {
51
+ beforeGet: () => void log.push("beforeGet"),
52
+ },
53
+ },
54
+ }));
55
+ const data = await hub.dataForPage("report", context());
56
+ assert.deepEqual(log, ["beforeGet", "head", "body"]);
57
+ assert.deepEqual(data, { head: { title: "Q1" }, body: { line: "one" } });
58
+ });
59
+ it("runs the query set when the page declares no hooks", async () => {
60
+ const log = [];
61
+ const hub = createHub(pages({ report: { queries: { head: stub(log, "head", { a: "1" }) } } }));
62
+ assert.deepEqual(await hub.dataForPage("report", context()), {
63
+ head: { a: "1" },
64
+ });
65
+ });
66
+ it("hands every query the context it was called with", async () => {
67
+ // The context is request-scoped — an authenticated database handle is the
68
+ // expected case — so which one a query gets is the whole of the contract.
69
+ seen.clear();
70
+ const log = [];
71
+ const hub = createHub(pages({
72
+ report: {
73
+ queries: {
74
+ head: stub(log, "head", {}),
75
+ body: stub(log, "body", {}),
76
+ },
77
+ },
78
+ }));
79
+ const ctx = context();
80
+ await hub.dataForPage("report", ctx);
81
+ assert.equal(seen.get("head"), ctx);
82
+ assert.equal(seen.get("body"), ctx);
83
+ });
84
+ it("answers nothing for a page it does not have, and says so", async () => {
85
+ const hub = createHub(pages({}));
86
+ const { result, warn } = await captured(() => hub.dataForPage("nope", context()));
87
+ assert.deepEqual(result, {});
88
+ assert.match(warn.join("\n"), /no page 'nope'/);
89
+ });
90
+ it("awaits a query that returns a promise", async () => {
91
+ const hub = createHub(pages({
92
+ report: { queries: { head: async () => ({ a: "1" }) } },
93
+ }));
94
+ assert.deepEqual(await hub.dataForPage("report", context()), {
95
+ head: { a: "1" },
96
+ });
97
+ });
98
+ });
99
+ // --- Actions -------------------------------------------------------------
100
+ describe("an action", () => {
101
+ it("is refused unless the page declared it", async () => {
102
+ // The rule that keeps this from being an RPC endpoint: the wire cannot
103
+ // reach anything the page has not published.
104
+ const log = [];
105
+ const hub = createHub(pages({
106
+ counter: {
107
+ queries: { count: stub(log, "count", { n: "1" }) },
108
+ hooks: {
109
+ actions: {
110
+ bump: { run: () => void log.push("bump"), refresh: ["count"] },
111
+ },
112
+ },
113
+ },
114
+ }));
115
+ const { result, warn } = await captured(() => hub.runAction("counter", "drop", {}, context()));
116
+ assert.deepEqual(result, {});
117
+ assert.deepEqual(log, [], "nothing ran");
118
+ assert.match(warn.join("\n"), /page 'counter' declares no action 'drop'/);
119
+ });
120
+ it("is refused on a page that does not exist", async () => {
121
+ const hub = createHub(pages({}));
122
+ const { result, warn } = await captured(() => hub.runAction("nope", "bump", {}, context()));
123
+ assert.deepEqual(result, {});
124
+ assert.match(warn.join("\n"), /declares no action 'bump'/);
125
+ });
126
+ it("runs, then runs the refresh set it declared", async () => {
127
+ const log = [];
128
+ const hub = createHub(pages({
129
+ counter: {
130
+ queries: {
131
+ count: stub(log, "count", { n: "1" }),
132
+ unrelated: stub(log, "unrelated", { x: "9" }),
133
+ },
134
+ hooks: {
135
+ actions: {
136
+ bump: { run: () => void log.push("bump"), refresh: ["count"] },
137
+ },
138
+ },
139
+ },
140
+ }));
141
+ const data = await hub.runAction("counter", "bump", {}, context());
142
+ assert.deepEqual(log, ["bump", "count"], "the action runs first");
143
+ assert.deepEqual(data, { count: { n: "1" } }, "only the refresh set ran");
144
+ });
145
+ it("hands the action where the interaction happened", async () => {
146
+ let where;
147
+ const hub = createHub(pages({
148
+ roster: {
149
+ queries: {},
150
+ hooks: {
151
+ actions: {
152
+ pick: { run: (_ctx, w) => void (where = w), refresh: [] },
153
+ },
154
+ },
155
+ },
156
+ }));
157
+ await hub.runAction("roster", "pick", { query: "members", key: "7", cell: "role", value: "lead" }, context());
158
+ assert.deepEqual(where, {
159
+ query: "members",
160
+ key: "7",
161
+ cell: "role",
162
+ value: "lead",
163
+ });
164
+ });
165
+ it("hands the action and its refresh queries one context", async () => {
166
+ seen.clear();
167
+ const log = [];
168
+ let actionCtx;
169
+ const hub = createHub(pages({
170
+ counter: {
171
+ queries: { count: stub(log, "count", {}) },
172
+ hooks: {
173
+ actions: {
174
+ bump: {
175
+ run: (ctx) => void (actionCtx = ctx),
176
+ refresh: ["count"],
177
+ },
178
+ },
179
+ },
180
+ },
181
+ }));
182
+ const ctx = context();
183
+ await hub.runAction("counter", "bump", {}, ctx);
184
+ assert.equal(actionCtx, ctx);
185
+ assert.equal(seen.get("count"), ctx);
186
+ });
187
+ it("refreshes against whatever the action wrote", async () => {
188
+ // The refresh queries run after the action, against the same context, so
189
+ // they see it. This is the case a store makes concrete.
190
+ const store = { n: 1 };
191
+ const hub = createHub(pages({
192
+ counter: {
193
+ queries: { count: () => ({ n: String(store.n) }) },
194
+ hooks: {
195
+ actions: {
196
+ bump: { run: () => void (store.n += 1), refresh: ["count"] },
197
+ },
198
+ },
199
+ },
200
+ }));
201
+ assert.deepEqual(await hub.runAction("counter", "bump", {}, context()), {
202
+ count: { n: "2" },
203
+ });
204
+ });
205
+ it("lays what it stated over what the refresh produced", async () => {
206
+ // This is how a patch reaches the browser at all. A query answers for its
207
+ // whole set and cannot know why it was re-run; the action knows exactly
208
+ // what it changed and may say only that. The narrower answer wins.
209
+ const hub = createHub(pages({
210
+ roster: {
211
+ queries: { members: () => rows([{ id: "1" }, { id: "2" }]) },
212
+ hooks: {
213
+ actions: {
214
+ rename: {
215
+ run: () => ({ members: patch({ rows: [{ id: "2" }] }) }),
216
+ refresh: ["members"],
217
+ },
218
+ },
219
+ },
220
+ },
221
+ }));
222
+ const data = await hub.runAction("roster", "rename", {}, context());
223
+ assert.deepEqual(data, { members: { op: "patch", rows: [{ id: "2" }] } });
224
+ });
225
+ it("answers with the refresh set alone when it states nothing", async () => {
226
+ const hub = createHub(pages({
227
+ counter: {
228
+ queries: { count: () => ({ n: "1" }) },
229
+ hooks: { actions: { bump: { run: () => { }, refresh: ["count"] } } },
230
+ },
231
+ }));
232
+ assert.deepEqual(await hub.runAction("counter", "bump", {}, context()), {
233
+ count: { n: "1" },
234
+ });
235
+ });
236
+ it("skips a refresh name the page does not declare, and runs its siblings", async () => {
237
+ const hub = createHub(pages({
238
+ counter: {
239
+ queries: { count: () => ({ n: "1" }) },
240
+ hooks: {
241
+ actions: {
242
+ bump: { run: () => { }, refresh: ["ghost", "count"] },
243
+ },
244
+ },
245
+ },
246
+ }));
247
+ const { result, warn } = await captured(() => hub.runAction("counter", "bump", {}, context()));
248
+ assert.deepEqual(result, { count: { n: "1" } });
249
+ assert.match(warn.join("\n"), /page 'counter' declares no query 'ghost'/);
250
+ });
251
+ it("awaits an action that returns a promise", async () => {
252
+ const action = {
253
+ run: async () => ({ count: { n: "5" } }),
254
+ refresh: [],
255
+ };
256
+ const hub = createHub(pages({ counter: { queries: {}, hooks: { actions: { bump: action } } } }));
257
+ assert.deepEqual(await hub.runAction("counter", "bump", {}, context()), {
258
+ count: { n: "5" },
259
+ });
260
+ });
261
+ });
262
+ // --- Cell change -----------------------------------------------------------
263
+ describe("a cell-change", () => {
264
+ it("is refused unless the query declared it", async () => {
265
+ const hub = createHub(pages({ roster: { queries: { members: () => rows([]) } } }));
266
+ const { result, warn } = await captured(() => hub.runCellChange("roster", { query: "members", key: "1", cell: "name", value: "Ada" }, context()));
267
+ assert.deepEqual(result, {});
268
+ assert.match(warn.join("\n"), /page 'roster' declares no cell-change for query 'members'/);
269
+ });
270
+ it("hands the operation its key, cell and value, then runs its refresh set", async () => {
271
+ let where;
272
+ const log = [];
273
+ const hub = createHub(pages({
274
+ roster: {
275
+ queries: { members: stub(log, "members", { n: "1" }) },
276
+ hooks: {
277
+ crud: {
278
+ members: {
279
+ cellChange: {
280
+ run: (_ctx, w) => void (where = w),
281
+ refresh: ["members"],
282
+ },
283
+ },
284
+ },
285
+ },
286
+ },
287
+ }));
288
+ const data = await hub.runCellChange("roster", { query: "members", key: "7", cell: "role", value: "lead" }, context());
289
+ assert.deepEqual(where, { key: "7", cell: "role", value: "lead" });
290
+ assert.deepEqual(log, ["members"]);
291
+ assert.deepEqual(data, { members: { n: "1" } });
292
+ });
293
+ it("lays what it stated over what the refresh produced", async () => {
294
+ const hub = createHub(pages({
295
+ roster: {
296
+ queries: { members: () => rows([{ id: "1" }, { id: "2" }]) },
297
+ hooks: {
298
+ crud: {
299
+ members: {
300
+ cellChange: {
301
+ run: (_ctx, w) => ({
302
+ members: patch({ rows: [{ id: "2", name: w.value }] }),
303
+ }),
304
+ refresh: ["members"],
305
+ },
306
+ },
307
+ },
308
+ },
309
+ },
310
+ }));
311
+ const data = await hub.runCellChange("roster", { query: "members", key: "2", cell: "name", value: "Grace" }, context());
312
+ assert.deepEqual(data, {
313
+ members: { op: "patch", rows: [{ id: "2", name: "Grace" }] },
314
+ });
315
+ });
316
+ });
317
+ // --- Tuple delete ------------------------------------------------------
318
+ describe("a tuple-delete", () => {
319
+ it("is refused unless the query declared it", async () => {
320
+ const hub = createHub(pages({ roster: { queries: { members: () => rows([]) } } }));
321
+ const { result, warn } = await captured(() => hub.runTupleDelete("roster", { query: "members", key: "1" }, context()));
322
+ assert.deepEqual(result, {});
323
+ assert.match(warn.join("\n"), /page 'roster' declares no tuple-delete for query 'members'/);
324
+ });
325
+ it("hands the operation its key, then runs its refresh set", async () => {
326
+ let where;
327
+ const hub = createHub(pages({
328
+ roster: {
329
+ queries: { members: () => rows([]) },
330
+ hooks: {
331
+ crud: {
332
+ members: {
333
+ tupleDelete: {
334
+ run: (_ctx, w) => void (where = w),
335
+ refresh: [],
336
+ },
337
+ },
338
+ },
339
+ },
340
+ },
341
+ }));
342
+ await hub.runTupleDelete("roster", { query: "members", key: "7" }, context());
343
+ assert.deepEqual(where, { key: "7" });
344
+ });
345
+ it("answers with the drop it stated", async () => {
346
+ const hub = createHub(pages({
347
+ roster: {
348
+ queries: { members: () => rows([{ id: "1" }, { id: "2" }]) },
349
+ hooks: {
350
+ crud: {
351
+ members: {
352
+ tupleDelete: {
353
+ run: (_ctx, w) => ({
354
+ members: patch({ drop: [w.key] }),
355
+ }),
356
+ refresh: [],
357
+ },
358
+ },
359
+ },
360
+ },
361
+ },
362
+ }));
363
+ const data = await hub.runTupleDelete("roster", { query: "members", key: "1" }, context());
364
+ assert.deepEqual(data, { members: { op: "patch", drop: ["1"] } });
365
+ });
366
+ });
367
+ // --- Tuple insert --------------------------------------------------------
368
+ describe("a tuple-insert", () => {
369
+ it("is refused unless the query declared it", async () => {
370
+ const hub = createHub(pages({ roster: { queries: { members: () => rows([]) } } }));
371
+ const { result, warn } = await captured(() => hub.runTupleInsert("roster", { query: "members", values: { name: "Ada" } }, context()));
372
+ assert.deepEqual(result, {});
373
+ assert.match(warn.join("\n"), /page 'roster' declares no tuple-insert for query 'members'/);
374
+ });
375
+ it("hands the operation the gathered values, then runs its refresh set", async () => {
376
+ let where;
377
+ const hub = createHub(pages({
378
+ roster: {
379
+ queries: { members: () => rows([]) },
380
+ hooks: {
381
+ crud: {
382
+ members: {
383
+ tupleInsert: {
384
+ run: (_ctx, w) => void (where = w),
385
+ refresh: [],
386
+ },
387
+ },
388
+ },
389
+ },
390
+ },
391
+ }));
392
+ await hub.runTupleInsert("roster", { query: "members", values: { name: "Ada", role: "Analyst" } }, context());
393
+ assert.deepEqual(where, { values: { name: "Ada", role: "Analyst" } });
394
+ });
395
+ it("answers with the row it stated", async () => {
396
+ const hub = createHub(pages({
397
+ roster: {
398
+ queries: { members: () => rows([{ id: "1" }]) },
399
+ hooks: {
400
+ crud: {
401
+ members: {
402
+ tupleInsert: {
403
+ run: (_ctx, w) => ({
404
+ members: patch({ rows: [{ id: "2", ...w.values }] }),
405
+ }),
406
+ refresh: [],
407
+ },
408
+ },
409
+ },
410
+ },
411
+ },
412
+ }));
413
+ const data = await hub.runTupleInsert("roster", { query: "members", values: { name: "Ada" } }, context());
414
+ assert.deepEqual(data, {
415
+ members: { op: "patch", rows: [{ id: "2", name: "Ada" }] },
416
+ });
417
+ });
418
+ });
419
+ // --- Tuple update ----------------------------------------------------------
420
+ describe("a tuple-update", () => {
421
+ it("is refused unless the query declared it", async () => {
422
+ const hub = createHub(pages({ roster: { queries: { members: () => rows([]) } } }));
423
+ const { result, warn } = await captured(() => hub.runTupleUpdate("roster", { query: "members", key: "1", values: { name: "Ada" } }, context()));
424
+ assert.deepEqual(result, {});
425
+ assert.match(warn.join("\n"), /page 'roster' declares no tuple-update for query 'members'/);
426
+ });
427
+ it("hands the operation the key and the gathered values, then runs its refresh set", async () => {
428
+ let where;
429
+ const hub = createHub(pages({
430
+ roster: {
431
+ queries: { members: () => rows([]) },
432
+ hooks: {
433
+ crud: {
434
+ members: {
435
+ tupleUpdate: {
436
+ run: (_ctx, w) => void (where = w),
437
+ refresh: [],
438
+ },
439
+ },
440
+ },
441
+ },
442
+ },
443
+ }));
444
+ await hub.runTupleUpdate("roster", { query: "members", key: "7", values: { name: "Ada", role: "Analyst" } }, context());
445
+ assert.deepEqual(where, {
446
+ key: "7",
447
+ values: { name: "Ada", role: "Analyst" },
448
+ });
449
+ });
450
+ it("answers with the row it stated", async () => {
451
+ const hub = createHub(pages({
452
+ roster: {
453
+ queries: { members: () => rows([{ id: "1", name: "old" }]) },
454
+ hooks: {
455
+ crud: {
456
+ members: {
457
+ tupleUpdate: {
458
+ run: (_ctx, w) => ({
459
+ members: patch({ rows: [{ id: w.key, ...w.values }] }),
460
+ }),
461
+ refresh: [],
462
+ },
463
+ },
464
+ },
465
+ },
466
+ },
467
+ }));
468
+ const data = await hub.runTupleUpdate("roster", { query: "members", key: "1", values: { name: "Ada" } }, context());
469
+ assert.deepEqual(data, {
470
+ members: { op: "patch", rows: [{ id: "1", name: "Ada" }] },
471
+ });
472
+ });
473
+ });
474
+ // --- The wire vocabulary -------------------------------------------------
475
+ describe("the projection helpers", () => {
476
+ it("name the whole set", () => {
477
+ assert.deepEqual(rows([{ id: "1" }]), { op: "rows", rows: [{ id: "1" }] });
478
+ });
479
+ it("name only what changed, and carry nothing they were not given", () => {
480
+ assert.deepEqual(patch({ drop: ["1"] }), { op: "patch", drop: ["1"] });
481
+ assert.deepEqual(patch({}), { op: "patch" });
482
+ });
483
+ it("are what isProjection recognises", () => {
484
+ assert.equal(isProjection(rows([])), true);
485
+ assert.equal(isProjection(patch({})), true);
486
+ assert.equal(isProjection({ id: "1", name: "Ada" }), false);
487
+ });
488
+ it("share one word with a tuple's cells, which reserves it", () => {
489
+ // Stated in core/lb-types.ts: `op` is a reserved cell name, because a
490
+ // tuple carrying one whose value is 'rows' or 'patch' is read as a
491
+ // projection. The hazard is real and this is where it is written down.
492
+ assert.equal(isProjection({ op: "rows" }), true);
493
+ assert.equal(isProjection({ op: "deleted" }), false);
494
+ });
495
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * lb-table — see docs/testing.md, tier 4.
3
+ *
4
+ * What this widget adds at run time is placement, and only placement:
5
+ * `lb-sort` names the column rows are ordered by, `lb-group` names the
6
+ * column they are sectioned by. Both answers are local, which is the whole
7
+ * argument for the Place callback existing.
8
+ *
9
+ * The markup below is what widgets/lb-table.html expands to once the page
10
+ * has supplied its head and its row template — a real <thead> holding a real
11
+ * <tr>, because expansion unwrapped the templates that carried them past the
12
+ * parser.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=lb-table.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-table.test.d.ts","sourceRoot":"","sources":["../../tests/lb-table.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}