@loadbare/app 0.5.6 → 0.7.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 (143) 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 +81 -7
  5. package/dist/build/assemble.js.map +1 -0
  6. package/dist/build/cli.d.ts +2 -2
  7. package/dist/build/cli.js +3 -2
  8. package/dist/build/cli.js.map +1 -0
  9. package/dist/build/elements.js +1 -0
  10. package/dist/build/elements.js.map +1 -0
  11. package/dist/build/expand.d.ts.map +1 -1
  12. package/dist/build/expand.js +20 -32
  13. package/dist/build/expand.js.map +1 -0
  14. package/dist/build/format.js +1 -0
  15. package/dist/build/format.js.map +1 -0
  16. package/dist/build/locations.d.ts +4 -4
  17. package/dist/build/locations.d.ts.map +1 -1
  18. package/dist/build/locations.js +16 -5
  19. package/dist/build/locations.js.map +1 -0
  20. package/dist/build/origins.d.ts +0 -13
  21. package/dist/build/origins.d.ts.map +1 -1
  22. package/dist/build/origins.js +33 -8
  23. package/dist/build/origins.js.map +1 -0
  24. package/dist/build/package-root.js +1 -0
  25. package/dist/build/package-root.js.map +1 -0
  26. package/dist/build/pages.d.ts +7 -3
  27. package/dist/build/pages.d.ts.map +1 -1
  28. package/dist/build/pages.js +14 -7
  29. package/dist/build/pages.js.map +1 -0
  30. package/dist/build/styles.js +1 -0
  31. package/dist/build/styles.js.map +1 -0
  32. package/dist/core/lb-constants.d.ts +15 -13
  33. package/dist/core/lb-constants.d.ts.map +1 -1
  34. package/dist/core/lb-constants.js +104 -53
  35. package/dist/core/lb-constants.js.map +1 -0
  36. package/dist/core/lb-types.d.ts +103 -62
  37. package/dist/core/lb-types.d.ts.map +1 -1
  38. package/dist/core/lb-types.js +12 -3
  39. package/dist/core/lb-types.js.map +1 -0
  40. package/dist/hub/lb-apply.d.ts +28 -4
  41. package/dist/hub/lb-apply.d.ts.map +1 -1
  42. package/dist/hub/lb-apply.js +227 -40
  43. package/dist/hub/lb-apply.js.map +1 -0
  44. package/dist/hub/lb-hub.browser.d.ts +1 -1
  45. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  46. package/dist/hub/lb-hub.browser.js +165 -110
  47. package/dist/hub/lb-hub.browser.js.map +1 -0
  48. package/dist/server/lb-express.d.ts +8 -5
  49. package/dist/server/lb-express.d.ts.map +1 -1
  50. package/dist/server/lb-express.js +46 -33
  51. package/dist/server/lb-express.js.map +1 -0
  52. package/dist/server/lb-server.d.ts +67 -45
  53. package/dist/server/lb-server.d.ts.map +1 -1
  54. package/dist/server/lb-server.js +56 -18
  55. package/dist/server/lb-server.js.map +1 -0
  56. package/docs/TECHREF-1.0.md +1107 -0
  57. package/docs/analysis-accidental-complexity.md +149 -0
  58. package/docs/reference/builder.md +4 -4
  59. package/docs/reference/chrome.md +10 -9
  60. package/docs/reference/custom-elements.md +54 -46
  61. package/docs/reference/data-binding.md +179 -97
  62. package/docs/reference/overview.md +1 -1
  63. package/docs/reference/page-files.md +64 -49
  64. package/docs/reference/server.md +25 -6
  65. package/docs/reference/widgets.md +22 -30
  66. package/docs/roadmap.md +68 -22
  67. package/docs/testing.md +47 -17
  68. package/docs/theory.md +116 -3
  69. package/docs/tutorials/010-pages-and-navigation.md +8 -8
  70. package/docs/tutorials/040-displaying-data.md +9 -9
  71. package/docs/tutorials/050-actions.md +5 -5
  72. package/docs/tutorials/060-custom-element-code.md +1 -1
  73. package/docs/tutorials/065-conditional-rendering.md +4 -4
  74. package/docs/tutorials/070-displaying-a-list.md +24 -47
  75. package/docs/tutorials/072-inserting-into-a-list.md +18 -15
  76. package/docs/tutorials/074-deleting-from-a-list.md +15 -17
  77. package/docs/tutorials/076-updating-a-list-item.md +20 -22
  78. package/docs/tutorials/080-widget-requests.md +22 -35
  79. package/docs/tutorials/090-using-widget-libraries.md +1 -1
  80. package/package.json +2 -3
  81. package/dist/hub/lb-rows.d.ts +0 -18
  82. package/dist/hub/lb-rows.d.ts.map +0 -1
  83. package/dist/hub/lb-rows.js +0 -106
  84. package/dist/tests/assemble.test.d.ts +0 -8
  85. package/dist/tests/assemble.test.d.ts.map +0 -1
  86. package/dist/tests/assemble.test.js +0 -58
  87. package/dist/tests/elements.test.d.ts +0 -8
  88. package/dist/tests/elements.test.d.ts.map +0 -1
  89. package/dist/tests/elements.test.js +0 -118
  90. package/dist/tests/expand.test.d.ts +0 -10
  91. package/dist/tests/expand.test.d.ts.map +0 -1
  92. package/dist/tests/expand.test.js +0 -250
  93. package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
  94. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
  95. package/dist/tests/fixtures/elements/collision/imports.js +0 -1
  96. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
  97. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
  98. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
  99. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
  100. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
  101. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
  102. package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
  103. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
  104. package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
  105. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
  106. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
  107. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
  108. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
  109. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
  110. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
  111. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
  112. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
  113. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
  114. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
  115. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
  116. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
  117. package/dist/tests/helpers/console.d.ts +0 -20
  118. package/dist/tests/helpers/console.d.ts.map +0 -1
  119. package/dist/tests/helpers/console.js +0 -28
  120. package/dist/tests/helpers/dom.d.ts +0 -18
  121. package/dist/tests/helpers/dom.d.ts.map +0 -1
  122. package/dist/tests/helpers/dom.js +0 -22
  123. package/dist/tests/lb-apply.test.d.ts +0 -8
  124. package/dist/tests/lb-apply.test.d.ts.map +0 -1
  125. package/dist/tests/lb-apply.test.js +0 -153
  126. package/dist/tests/lb-express.test.d.ts +0 -14
  127. package/dist/tests/lb-express.test.d.ts.map +0 -1
  128. package/dist/tests/lb-express.test.js +0 -238
  129. package/dist/tests/lb-rows.test.d.ts +0 -12
  130. package/dist/tests/lb-rows.test.d.ts.map +0 -1
  131. package/dist/tests/lb-rows.test.js +0 -336
  132. package/dist/tests/lb-server.test.d.ts +0 -9
  133. package/dist/tests/lb-server.test.d.ts.map +0 -1
  134. package/dist/tests/lb-server.test.js +0 -495
  135. package/dist/tests/origins.test.d.ts +0 -10
  136. package/dist/tests/origins.test.d.ts.map +0 -1
  137. package/dist/tests/origins.test.js +0 -369
  138. package/dist/tests/pages.test.d.ts +0 -6
  139. package/dist/tests/pages.test.d.ts.map +0 -1
  140. package/dist/tests/pages.test.js +0 -98
  141. package/dist/tests/styles.test.d.ts +0 -7
  142. package/dist/tests/styles.test.d.ts.map +0 -1
  143. package/dist/tests/styles.test.js +0 -76
@@ -1,22 +0,0 @@
1
- /**
2
- * A document for tier 3 — see docs/testing.md.
3
- *
4
- * The landing code touches nothing global: it is handed a root and calls
5
- * `matches`, `querySelectorAll`, `cloneNode` and `setAttribute` on it. So a
6
- * test needs a parsed tree and nothing else — no globals installed, no
7
- * custom elements registered, no window on the loose between test files.
8
- *
9
- * Markup is parsed as the body of a document rather than into a detached
10
- * `<div>`, because the parser's content models are half of what this code
11
- * exists to work around: a `<tr>` assigned to a div's innerHTML is discarded,
12
- * and a test that lost its rows would be testing the parser.
13
- */
14
- import { JSDOM } from "jsdom";
15
- /** Parse markup and hand back the body holding it. */
16
- export function body(html) {
17
- return new JSDOM(`<!doctype html><body>${html}</body>`).window.document.body;
18
- }
19
- /** Parse markup and hand back its first element. */
20
- export function one(html) {
21
- return body(html).firstElementChild;
22
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Landing data on a live host — see docs/testing.md, tier 3.
3
- *
4
- * hub/lb-apply.ts is pure DOM: no fetch, no widget upgrade, no history.
5
- * jsdom is real evidence for it, which is not true one file further on.
6
- */
7
- export {};
8
- //# sourceMappingURL=lb-apply.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-apply.test.d.ts","sourceRoot":"","sources":["../../tests/lb-apply.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,153 +0,0 @@
1
- /**
2
- * Landing data on a live host — see docs/testing.md, tier 3.
3
- *
4
- * hub/lb-apply.ts is pure DOM: no fetch, no widget upgrade, no history.
5
- * jsdom is real evidence for it, which is not true one file further on.
6
- */
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import { applyData, applyTuple } from "../hub/lb-apply";
10
- import { body, one } from "./helpers/dom";
11
- import { captured } from "./helpers/console";
12
- /** An element that is a list widget as far as the hub is concerned. */
13
- function rowHost(html) {
14
- const el = one(html);
15
- el.seen = [];
16
- el.acceptRows = (result) => void el.seen.push(result);
17
- return el;
18
- }
19
- // --- One tuple into one scope -------------------------------------------
20
- describe("applyTuple", () => {
21
- it("gives a native element its value as text", () => {
22
- const scope = one(`<div><span lb-cell="name"></span></div>`);
23
- applyTuple(scope, { name: "Ada" });
24
- assert.equal(scope.querySelector("span").textContent, "Ada");
25
- });
26
- it("gives a widget its value as an attribute", () => {
27
- // A custom element owns whatever control it wraps, so it receives the
28
- // value and renders it itself. The test is lexical — a hyphen in the tag
29
- // name — and the hub holds no knowledge of any particular element.
30
- const scope = one(`<div><lb-input lb-cell="name"></lb-input></div>`);
31
- applyTuple(scope, { name: "Ada" });
32
- const widget = scope.querySelector("lb-input");
33
- assert.equal(widget.getAttribute("lb-value"), "Ada");
34
- assert.equal(widget.textContent, "", "the widget's children are its own");
35
- });
36
- it("counts the root as a cell when it carries one", () => {
37
- // <option>'s content model is text, so an option row has to be the cell
38
- // it displays. Requiring a wrapper would require an element HTML does
39
- // not allow.
40
- const scope = one(`<option lb-cell="name"></option>`);
41
- applyTuple(scope, { name: "Ada" });
42
- assert.equal(scope.textContent, "Ada");
43
- });
44
- it("fills every element naming the same cell", () => {
45
- const scope = one(`<div><b lb-cell="name"></b><i lb-cell="name"></i></div>`);
46
- applyTuple(scope, { name: "Ada" });
47
- assert.equal(scope.querySelector("b").textContent, "Ada");
48
- assert.equal(scope.querySelector("i").textContent, "Ada");
49
- });
50
- it("empties a native root that is a cell, children and all", () => {
51
- // A root that is a cell is a leaf, and a native one lands its value as
52
- // text — so the children go, because setting textContent is what
53
- // replacing an element's content means.
54
- //
55
- // The case this is for cannot hit it: <option> is a leaf by content
56
- // model. But nothing stops an author writing a cell on a <tr> that has
57
- // cells inside it, and this is what they would get.
58
- const scope = body(`<table><tbody><tr lb-cell="line"><td lb-cell="amount"></td></tr></tbody></table>`).querySelector("tr");
59
- applyTuple(scope, { line: "Total", amount: "10" });
60
- assert.equal(scope.textContent, "Total");
61
- assert.equal(scope.children.length, 0, "the amount cell is gone");
62
- });
63
- it("keeps the children of a widget root that is a cell", () => {
64
- // The same shape with a hyphen in the tag name is unharmed, because a
65
- // widget receives its value as an attribute and renders it itself. The
66
- // asymmetry follows from what a cell means on each kind of element.
67
- const scope = one(`<lb-input lb-cell="line"><input /></lb-input>`);
68
- applyTuple(scope, { line: "Total" });
69
- assert.equal(scope.getAttribute("lb-value"), "Total");
70
- assert.equal(scope.children.length, 1);
71
- });
72
- it("leaves a cell the tuple did not name alone", () => {
73
- const scope = one(`<div><span lb-cell="name">Ada</span><span lb-cell="role">lead</span></div>`);
74
- applyTuple(scope, { role: "staff" });
75
- assert.equal(scope.querySelector(`[lb-cell="name"]`).textContent, "Ada");
76
- assert.equal(scope.querySelector(`[lb-cell="role"]`).textContent, "staff");
77
- });
78
- it("says nothing about a cell with nowhere to land", () => {
79
- const scope = one(`<div><span lb-cell="name"></span></div>`);
80
- assert.doesNotThrow(() => applyTuple(scope, { absent: "x" }));
81
- });
82
- it("lands a value as text, so data cannot become markup", () => {
83
- // The same guarantee expansion makes at build time, made here at run
84
- // time and by the same means: the DOM API rather than a string.
85
- const scope = one(`<div><span lb-cell="name"></span></div>`);
86
- applyTuple(scope, { name: `<img src=x onerror=alert(1)>` });
87
- assert.equal(scope.querySelectorAll("img").length, 0);
88
- assert.equal(scope.querySelector("span").textContent, `<img src=x onerror=alert(1)>`);
89
- });
90
- it("lands the empty string as the empty string", () => {
91
- const scope = one(`<div><span lb-cell="name">Ada</span></div>`);
92
- applyTuple(scope, { name: "" });
93
- assert.equal(scope.querySelector("span").textContent, "");
94
- });
95
- });
96
- // --- A whole response ----------------------------------------------------
97
- describe("applyData", () => {
98
- it("lands each result in the scope its query names", async () => {
99
- const root = body(`<div lb-query="greeting"><span lb-cell="text"></span></div>` +
100
- `<div lb-query="count"><span lb-cell="n"></span></div>`);
101
- applyData(root, { greeting: { text: "hello" }, count: { n: "3" } });
102
- assert.equal(root.querySelector(`[lb-query="greeting"] span`).textContent, "hello");
103
- assert.equal(root.querySelector(`[lb-query="count"] span`).textContent, "3");
104
- });
105
- it("fills every scope naming the same query", () => {
106
- // Two projections of one query on one page: the hub resolves by name and
107
- // does not care how many places asked.
108
- const root = body(`<div lb-query="total"><span lb-cell="n"></span></div>` +
109
- `<p lb-query="total"><span lb-cell="n"></span></p>`);
110
- applyData(root, { total: { n: "7" } });
111
- for (const el of root.querySelectorAll("span")) {
112
- assert.equal(el.textContent, "7");
113
- }
114
- });
115
- it("reports a query with no scope, and lands the rest anyway", async () => {
116
- const root = body(`<div lb-query="count"><span lb-cell="n"></span></div>`);
117
- const { warn } = await captured(() => applyData(root, { ghost: { x: "1" }, count: { n: "3" } }));
118
- assert.match(warn.join("\n"), /no scope for query 'ghost', skipping/);
119
- assert.equal(root.querySelector("span").textContent, "3");
120
- });
121
- it("hands a projection to the widget, whole", () => {
122
- const host = rowHost(`<lb-list lb-query="members"></lb-list>`);
123
- const root = body("");
124
- root.append(host);
125
- const result = { op: "rows", rows: [{ id: "1" }] };
126
- applyData(root, { members: result });
127
- assert.deepEqual(host.seen, [result]);
128
- });
129
- it("reports a projection landing on something that is not a list", async () => {
130
- // A native element has one destination for a value and no way to acquire
131
- // children, so a list is not something it can be asked to show.
132
- const root = body(`<div lb-query="members"></div>`);
133
- const { error } = await captured(() => applyData(root, { members: { op: "rows", rows: [{ id: "1" }] } }));
134
- assert.match(error.join("\n"), /query 'members' returned rows, but <div> is not a list widget/);
135
- });
136
- it("reads a tuple carrying a cell called op as a projection", async () => {
137
- // The reserved name, from the browser's side. Stated in
138
- // core/lb-types.ts and worth failing loudly if it ever moves.
139
- const root = body(`<div lb-query="members"></div>`);
140
- const { error } = await captured(() => applyData(root, { members: { op: "rows" } }));
141
- assert.match(error.join("\n"), /is not a list widget/);
142
- });
143
- it("lands a tuple whose op cell is not a projection word", () => {
144
- const root = body(`<div lb-query="row"><span lb-cell="op"></span></div>`);
145
- applyData(root, { row: { op: "deleted" } });
146
- assert.equal(root.querySelector("span").textContent, "deleted");
147
- });
148
- it("does nothing with an empty response", () => {
149
- const root = body(`<div lb-query="count"><span lb-cell="n">3</span></div>`);
150
- applyData(root, {});
151
- assert.equal(root.querySelector("span").textContent, "3");
152
- });
153
- });
@@ -1,14 +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: two endpoints and no third, 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
- export {};
14
- //# sourceMappingURL=lb-express.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-express.test.d.ts","sourceRoot":"","sources":["../../tests/lb-express.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -1,238 +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: two endpoints and no third, 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_DATA_ENDPOINT, LB_REQUEST_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 runTupleDelete(page, where, ctx) {
37
- calls.push({ kind: "tuple-delete", page, where, ctx });
38
- return { members: { n: "4" } };
39
- },
40
- async runTupleInsert(page, where, ctx) {
41
- calls.push({ kind: "tuple-insert", page, where, ctx });
42
- return { members: { n: "5" } };
43
- },
44
- async runTupleUpdate(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_REQUEST_ENDPOINT}?page=${page}`, {
65
- method: "POST",
66
- headers: { "content-type": "application/json" },
67
- body: raw ?? JSON.stringify(body),
68
- });
69
- }
70
- describe("the data endpoint", () => {
71
- it("runs the page named in the query string", async () => {
72
- calls.length = 0;
73
- const res = await fetch(`${base}${LB_DATA_ENDPOINT}?page=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_DATA_ENDPOINT}`);
85
- assert.equal(calls[0].page, "");
86
- });
87
- });
88
- describe("the request endpoint", () => {
89
- it("runs the action, with where the interaction happened", async () => {
90
- calls.length = 0;
91
- const res = await post("roster", {
92
- op: "action",
93
- name: "pick",
94
- query: "members",
95
- key: "7",
96
- cell: "role",
97
- value: "lead",
98
- });
99
- assert.equal(res.status, 200);
100
- assert.deepEqual(await res.json(), { count: { n: "2" } });
101
- assert.deepEqual(calls[0].where, {
102
- query: "members",
103
- key: "7",
104
- cell: "role",
105
- value: "lead",
106
- });
107
- assert.equal(calls[0].name, "pick");
108
- assert.equal(calls[0].page, "roster");
109
- });
110
- it("passes on an action carrying no binding at all", async () => {
111
- // A button carries no value and may sit outside any scope. Undefined
112
- // stays undefined rather than becoming the empty string.
113
- calls.length = 0;
114
- await post("counter", { op: "action", name: "bump" });
115
- assert.deepEqual(calls[0].where, {
116
- query: undefined,
117
- key: undefined,
118
- cell: undefined,
119
- value: undefined,
120
- });
121
- });
122
- it("refuses an op it does not recognise, rather than guessing", async () => {
123
- calls.length = 0;
124
- const { result: res, warn } = await captured(() => post("roster", { op: "tuple-upsert", query: "members", key: "1" }));
125
- assert.equal(res.status, 400);
126
- assert.deepEqual(await res.json(), {});
127
- assert.deepEqual(calls, [], "the engine was never reached");
128
- assert.match(warn.join("\n"), /operation 'tuple-upsert' not implemented/);
129
- });
130
- it("runs a cell-change, with where the interaction happened", async () => {
131
- calls.length = 0;
132
- const res = await post("roster", {
133
- op: "cell-change",
134
- query: "members",
135
- key: "7",
136
- cell: "name",
137
- value: "Ada",
138
- });
139
- assert.equal(res.status, 200);
140
- assert.deepEqual(await res.json(), { members: { n: "3" } });
141
- assert.deepEqual(calls[0].where, {
142
- query: "members",
143
- key: "7",
144
- cell: "name",
145
- value: "Ada",
146
- });
147
- assert.equal(calls[0].kind, "cell-change");
148
- assert.equal(calls[0].page, "roster");
149
- });
150
- it("runs a tuple-delete, with the key of the row", async () => {
151
- calls.length = 0;
152
- const res = await post("roster", {
153
- op: "tuple-delete",
154
- query: "members",
155
- key: "7",
156
- });
157
- assert.equal(res.status, 200);
158
- assert.deepEqual(await res.json(), { members: { n: "4" } });
159
- assert.deepEqual(calls[0].where, { query: "members", key: "7" });
160
- assert.equal(calls[0].kind, "tuple-delete");
161
- assert.equal(calls[0].page, "roster");
162
- });
163
- it("runs a tuple-insert, with the gathered values", async () => {
164
- calls.length = 0;
165
- const res = await post("roster", {
166
- op: "tuple-insert",
167
- query: "members",
168
- values: { name: "Ada", role: "Analyst" },
169
- });
170
- assert.equal(res.status, 200);
171
- assert.deepEqual(await res.json(), { members: { n: "5" } });
172
- assert.deepEqual(calls[0].where, {
173
- query: "members",
174
- values: { name: "Ada", role: "Analyst" },
175
- });
176
- assert.equal(calls[0].kind, "tuple-insert");
177
- assert.equal(calls[0].page, "roster");
178
- });
179
- it("runs a tuple-update, with the key and the gathered values", async () => {
180
- calls.length = 0;
181
- const res = await post("roster", {
182
- op: "tuple-update",
183
- query: "members",
184
- key: "7",
185
- values: { name: "Ada", role: "Analyst" },
186
- });
187
- assert.equal(res.status, 200);
188
- assert.deepEqual(await res.json(), { members: { n: "6" } });
189
- assert.deepEqual(calls[0].where, {
190
- query: "members",
191
- key: "7",
192
- values: { name: "Ada", role: "Analyst" },
193
- });
194
- assert.equal(calls[0].kind, "tuple-update");
195
- assert.equal(calls[0].page, "roster");
196
- });
197
- it("refuses a body with no op", async () => {
198
- calls.length = 0;
199
- const { result: res } = await captured(() => post("roster", { name: "pick" }));
200
- assert.equal(res.status, 400);
201
- assert.deepEqual(calls, []);
202
- });
203
- it("survives an operation that throws, rather than crashing", async () => {
204
- calls.length = 0;
205
- const { result: res, error } = await captured(() => post("roster", { op: "action", name: "explode", query: "members" }));
206
- assert.equal(res.status, 500);
207
- assert.deepEqual(await res.json(), {});
208
- assert.match(error.join("\n"), /operation 'action' threw/);
209
- // And the process is still serving.
210
- const next = await fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
211
- assert.equal(next.status, 200);
212
- });
213
- it("survives a body that is not JSON", async () => {
214
- calls.length = 0;
215
- const res = await post("roster", null, "{ not json");
216
- assert.ok(res.status >= 400, `expected a refusal, got ${res.status}`);
217
- assert.deepEqual(calls, []);
218
- // And the process is still serving. A parse failure is one request's
219
- // problem, not the server's.
220
- const next = await fetch(`${base}${LB_DATA_ENDPOINT}?page=hello`);
221
- assert.equal(next.status, 200);
222
- });
223
- });
224
- describe("the context", () => {
225
- it("is built once per request, by the application", async () => {
226
- // Hub's only requirement is that the context is built per request,
227
- // after whatever establishes identity. Holding one for the life of the
228
- // process is the mistake this asserts against.
229
- contexts = 0;
230
- 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" });
234
- assert.equal(contexts, 3);
235
- assert.notEqual(calls[0].ctx, calls[1].ctx);
236
- assert.notEqual(calls[1].ctx, calls[2].ctx);
237
- });
238
- });
@@ -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-rows.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-rows.test.d.ts","sourceRoot":"","sources":["../../tests/lb-rows.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}