@loadbare/app 0.4.0 → 0.5.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 (160) hide show
  1. package/README.md +53 -82
  2. package/dist/build/assemble.d.ts +7 -5
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +29 -9
  5. package/dist/build/cli.d.ts +20 -12
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +34 -16
  8. package/dist/build/elements.d.ts +15 -29
  9. package/dist/build/elements.d.ts.map +1 -1
  10. package/dist/build/elements.js +25 -111
  11. package/dist/build/expand.d.ts +1 -1
  12. package/dist/build/expand.js +1 -1
  13. package/dist/build/locations.d.ts +14 -37
  14. package/dist/build/locations.d.ts.map +1 -1
  15. package/dist/build/locations.js +24 -67
  16. package/dist/build/origins.d.ts +109 -0
  17. package/dist/build/origins.d.ts.map +1 -0
  18. package/dist/build/origins.js +270 -0
  19. package/dist/core/lb-constants.d.ts +1 -0
  20. package/dist/core/lb-constants.d.ts.map +1 -1
  21. package/dist/core/lb-constants.js +15 -8
  22. package/dist/core/lb-types.d.ts +2 -2
  23. package/dist/core/lb-types.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +1 -1
  25. package/dist/hub/lb-hub.d.ts.map +1 -1
  26. package/dist/hub/lb-hub.js +44 -17
  27. package/dist/hub/lb-rows.js +3 -3
  28. package/dist/server/lb-server.d.ts +5 -4
  29. package/dist/server/lb-server.d.ts.map +1 -1
  30. package/dist/tests/assemble.test.js +11 -4
  31. package/dist/tests/elements.test.js +47 -51
  32. package/dist/tests/expand.test.d.ts +1 -1
  33. package/dist/tests/expand.test.js +2 -2
  34. package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
  35. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
  36. package/dist/tests/fixtures/elements/collision/imports.js +1 -0
  37. package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
  38. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
  39. package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
  40. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
  41. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
  42. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
  43. package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
  44. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
  45. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
  46. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
  47. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
  48. package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
  49. package/dist/tests/lb-express.test.js +1 -1
  50. package/dist/tests/origins.test.d.ts +10 -0
  51. package/dist/tests/origins.test.d.ts.map +1 -0
  52. package/dist/tests/origins.test.js +326 -0
  53. package/dist/tests/pages.test.js +3 -3
  54. package/dist/tests/styles.test.js +7 -4
  55. package/docs/reference/builder.md +128 -0
  56. package/docs/reference/chrome.md +75 -0
  57. package/docs/reference/css.md +44 -0
  58. package/docs/reference/custom-elements.md +327 -0
  59. package/docs/reference/data-binding.md +240 -0
  60. package/docs/reference/overview.md +38 -0
  61. package/docs/reference/page-files.md +175 -0
  62. package/docs/reference/server.md +123 -0
  63. package/docs/reference/widgets.md +163 -0
  64. package/docs/roadmap.md +130 -0
  65. package/docs/testing.md +228 -0
  66. package/docs/theory.md +344 -223
  67. package/docs/tutorials/000-getting-started.md +86 -0
  68. package/docs/tutorials/010-pages-and-navigation.md +129 -0
  69. package/docs/tutorials/020-css.md +103 -0
  70. package/docs/tutorials/030-html-decomposition.md +79 -0
  71. package/docs/tutorials/040-displaying-data.md +169 -0
  72. package/docs/tutorials/050-actions.md +77 -0
  73. package/docs/tutorials/060-custom-element-code.md +73 -0
  74. package/docs/tutorials/065-conditional-rendering.md +161 -0
  75. package/docs/tutorials/070-displaying-a-list.md +137 -0
  76. package/docs/tutorials/072-inserting-into-a-list.md +88 -0
  77. package/docs/tutorials/074-deleting-from-a-list.md +77 -0
  78. package/docs/tutorials/076-updating-a-list-item.md +86 -0
  79. package/docs/tutorials/080-widget-requests.md +124 -0
  80. package/docs/tutorials/090-using-widget-libraries.md +75 -0
  81. package/package.json +4 -12
  82. package/dist/client.js +0 -522
  83. package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
  84. package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
  85. package/dist/demo-static/src/widgets/app-box.js +0 -19
  86. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
  87. package/dist/tests/fixtures/elements/collision/elements.js +0 -3
  88. package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
  89. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
  90. package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
  91. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
  92. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
  93. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
  94. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
  95. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
  96. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
  97. package/dist/tests/golden.test.d.ts +0 -19
  98. package/dist/tests/golden.test.d.ts.map +0 -1
  99. package/dist/tests/golden.test.js +0 -60
  100. package/dist/tests/helpers/window.d.ts +0 -43
  101. package/dist/tests/helpers/window.d.ts.map +0 -1
  102. package/dist/tests/helpers/window.js +0 -78
  103. package/dist/tests/lb-input.test.d.ts +0 -9
  104. package/dist/tests/lb-input.test.d.ts.map +0 -1
  105. package/dist/tests/lb-input.test.js +0 -78
  106. package/dist/tests/lb-list.test.d.ts +0 -12
  107. package/dist/tests/lb-list.test.d.ts.map +0 -1
  108. package/dist/tests/lb-list.test.js +0 -44
  109. package/dist/tests/lb-options.test.d.ts +0 -10
  110. package/dist/tests/lb-options.test.d.ts.map +0 -1
  111. package/dist/tests/lb-options.test.js +0 -121
  112. package/dist/tests/lb-picker.test.d.ts +0 -14
  113. package/dist/tests/lb-picker.test.d.ts.map +0 -1
  114. package/dist/tests/lb-picker.test.js +0 -59
  115. package/dist/tests/lb-select.test.d.ts +0 -9
  116. package/dist/tests/lb-select.test.d.ts.map +0 -1
  117. package/dist/tests/lb-select.test.js +0 -71
  118. package/dist/tests/lb-table.test.d.ts +0 -15
  119. package/dist/tests/lb-table.test.d.ts.map +0 -1
  120. package/dist/tests/lb-table.test.js +0 -205
  121. package/dist/widgets/index.d.ts +0 -7
  122. package/dist/widgets/index.d.ts.map +0 -1
  123. package/dist/widgets/index.js +0 -6
  124. package/dist/widgets/lb-input.d.ts +0 -2
  125. package/dist/widgets/lb-input.d.ts.map +0 -1
  126. package/dist/widgets/lb-input.js +0 -48
  127. package/dist/widgets/lb-list.d.ts +0 -2
  128. package/dist/widgets/lb-list.d.ts.map +0 -1
  129. package/dist/widgets/lb-list.js +0 -17
  130. package/dist/widgets/lb-options.d.ts +0 -26
  131. package/dist/widgets/lb-options.d.ts.map +0 -1
  132. package/dist/widgets/lb-options.js +0 -72
  133. package/dist/widgets/lb-picker.d.ts +0 -2
  134. package/dist/widgets/lb-picker.d.ts.map +0 -1
  135. package/dist/widgets/lb-picker.js +0 -25
  136. package/dist/widgets/lb-select.d.ts +0 -2
  137. package/dist/widgets/lb-select.d.ts.map +0 -1
  138. package/dist/widgets/lb-select.js +0 -43
  139. package/dist/widgets/lb-table.d.ts +0 -2
  140. package/dist/widgets/lb-table.d.ts.map +0 -1
  141. package/dist/widgets/lb-table.js +0 -113
  142. package/docs/application-chrome.md +0 -36
  143. package/docs/building-html-pages.md +0 -130
  144. package/docs/getting-started.md +0 -120
  145. package/docs/guide.md +0 -1164
  146. package/docs/hosting.md +0 -218
  147. package/docs/latent-risks.md +0 -20
  148. package/widgets/index.ts +0 -6
  149. package/widgets/lb-input.html +0 -1
  150. package/widgets/lb-input.ts +0 -64
  151. package/widgets/lb-list.html +0 -1
  152. package/widgets/lb-list.ts +0 -21
  153. package/widgets/lb-options.html +0 -4
  154. package/widgets/lb-options.ts +0 -88
  155. package/widgets/lb-picker.html +0 -7
  156. package/widgets/lb-picker.ts +0 -27
  157. package/widgets/lb-select.html +0 -4
  158. package/widgets/lb-select.ts +0 -55
  159. package/widgets/lb-table.html +0 -8
  160. package/widgets/lb-table.ts +0 -126
@@ -1,60 +0,0 @@
1
- /**
2
- * Golden files — see docs/testing.md, tier 1.
3
- *
4
- * Every page in demo/pages is expanded against the real definition
5
- * directories and compared to a stored copy of its output. Ten cheap tests
6
- * that cover the whole chain at once, including the directory ordering that
7
- * lets an application override a built-in widget.
8
- *
9
- * The stored copy is formatted, because a golden file nobody can read is a
10
- * diff nobody reviews. Formatting is cosmetic and may decline (see
11
- * build/format.ts); if it does, the comparison is still exact — both sides
12
- * went through the same function.
13
- *
14
- * npm run test:golden rewrite the stored copies
15
- *
16
- * Rewriting is not blessing. The diff is the point.
17
- */
18
- import assert from "node:assert/strict";
19
- import { readdir, readFile, writeFile } from "node:fs/promises";
20
- import { describe, it } from "node:test";
21
- import { fileURLToPath } from "node:url";
22
- import { expand, loadDefinitions } from "../build/expand";
23
- import { formatHtml } from "../build/format";
24
- const PAGES = fileURLToPath(new URL("../demo/pages", import.meta.url));
25
- const GOLDEN = fileURLToPath(new URL("./golden", import.meta.url));
26
- /** Built-in widgets first, then the demo's, exactly as demo/shell.ts orders them. */
27
- const DEFINITION_DIRS = [
28
- fileURLToPath(new URL("../widgets", import.meta.url)),
29
- fileURLToPath(new URL("../demo/widgets", import.meta.url)),
30
- ];
31
- const rewrite = process.env.UPDATE_GOLDEN === "1";
32
- const PAGE_SUFFIX = ".page.html";
33
- const defs = await loadDefinitions(DEFINITION_DIRS);
34
- const names = (await readdir(PAGES))
35
- .filter((name) => name.endsWith(PAGE_SUFFIX))
36
- .map((name) => name.slice(0, -PAGE_SUFFIX.length))
37
- .sort();
38
- describe("the demo pages expand to what they expanded to before", () => {
39
- assert.ok(names.length > 0, "no demo pages found to expand");
40
- for (const name of names) {
41
- it(name, async () => {
42
- const authored = await readFile(`${PAGES}/${name}${PAGE_SUFFIX}`, "utf-8");
43
- const expanded = await formatHtml(expand(authored, defs));
44
- const file = `${GOLDEN}/${name}.html`;
45
- if (rewrite) {
46
- await writeFile(file, expanded);
47
- return;
48
- }
49
- let stored;
50
- try {
51
- stored = await readFile(file, "utf-8");
52
- }
53
- catch {
54
- assert.fail(`no golden file for '${name}'. Review the expansion, then run ` +
55
- `npm run test:golden to store it.`);
56
- }
57
- assert.equal(expanded, stored);
58
- });
59
- }
60
- });
@@ -1,43 +0,0 @@
1
- /**
2
- * A window for the widget half of tier 4 — see docs/testing.md.
3
- *
4
- * Tier 3 needed no globals: the landing code is handed a root and calls
5
- * methods on it. A widget is the opposite. It extends `HTMLElement`, it
6
- * registers itself with `customElements` when its module loads, and it
7
- * reaches `document` to build the scaffolding it owns. So the window has to
8
- * exist before the module does, which is why widget modules are imported
9
- * dynamically, after `installWindow()` has run:
10
- *
11
- * installWindow();
12
- * await import("../widgets/index");
13
- *
14
- * One window per test file, which is what node:test gives us for free by
15
- * running each file in its own process. Registration is global and permanent,
16
- * so sharing a process across widget files would mean sharing a registry.
17
- *
18
- * What this buys is real upgrades: a widget's constructor, its
19
- * connectedCallback, and attributeChangedCallback for attributes already
20
- * present. That is most of what a widget is, and it is the same in jsdom as
21
- * in a browser. What it does not buy is layout, painting, or navigation —
22
- * which is why the hub is tested in a browser instead.
23
- */
24
- import type { HubRequest } from "../../core/lb-types";
25
- export declare function installWindow(): void;
26
- /**
27
- * Put markup in the document and hand back the first element of it.
28
- *
29
- * Connected, because a widget that is not in the document has not upgraded,
30
- * and half of what is under test happens on upgrade.
31
- */
32
- export declare function mount(html: string): Element;
33
- /**
34
- * Collect the requests a widget dispatches.
35
- *
36
- * Listening on the body rather than on the widget, because a request that
37
- * does not bubble never reaches the hub — the hub is an ancestor, and that
38
- * is the whole delivery mechanism.
39
- */
40
- export declare function requests(): HubRequest[];
41
- /** Fire the event a control fires when a person uses it. */
42
- export declare function change(el: Element): void;
43
- //# sourceMappingURL=window.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../../tests/helpers/window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAoBtD,wBAAgB,aAAa,IAAI,IAAI,CASpC;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3C;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,IAAI,UAAU,EAAE,CAMvC;AAED,4DAA4D;AAC5D,wBAAgB,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAExC"}
@@ -1,78 +0,0 @@
1
- /**
2
- * A window for the widget half of tier 4 — see docs/testing.md.
3
- *
4
- * Tier 3 needed no globals: the landing code is handed a root and calls
5
- * methods on it. A widget is the opposite. It extends `HTMLElement`, it
6
- * registers itself with `customElements` when its module loads, and it
7
- * reaches `document` to build the scaffolding it owns. So the window has to
8
- * exist before the module does, which is why widget modules are imported
9
- * dynamically, after `installWindow()` has run:
10
- *
11
- * installWindow();
12
- * await import("../widgets/index");
13
- *
14
- * One window per test file, which is what node:test gives us for free by
15
- * running each file in its own process. Registration is global and permanent,
16
- * so sharing a process across widget files would mean sharing a registry.
17
- *
18
- * What this buys is real upgrades: a widget's constructor, its
19
- * connectedCallback, and attributeChangedCallback for attributes already
20
- * present. That is most of what a widget is, and it is the same in jsdom as
21
- * in a browser. What it does not buy is layout, painting, or navigation —
22
- * which is why the hub is tested in a browser instead.
23
- */
24
- import { JSDOM } from "jsdom";
25
- import { LB_EVENT_NAME } from "../../core/lb-constants";
26
- /** The globals a widget module reaches for as it loads and runs. */
27
- const GLOBALS = [
28
- "window",
29
- "document",
30
- "customElements",
31
- "HTMLElement",
32
- "HTMLTemplateElement",
33
- "HTMLSelectElement",
34
- "HTMLOptGroupElement",
35
- "HTMLOptionElement",
36
- "Element",
37
- "Node",
38
- "Event",
39
- "CustomEvent",
40
- "MouseEvent",
41
- "DocumentFragment",
42
- ];
43
- export function installWindow() {
44
- const { window } = new JSDOM(`<!doctype html><body></body>`, {
45
- url: "http://localhost/",
46
- });
47
- for (const name of GLOBALS) {
48
- globalThis[name] = window[name];
49
- }
50
- }
51
- /**
52
- * Put markup in the document and hand back the first element of it.
53
- *
54
- * Connected, because a widget that is not in the document has not upgraded,
55
- * and half of what is under test happens on upgrade.
56
- */
57
- export function mount(html) {
58
- document.body.innerHTML = html;
59
- return document.body.firstElementChild;
60
- }
61
- /**
62
- * Collect the requests a widget dispatches.
63
- *
64
- * Listening on the body rather than on the widget, because a request that
65
- * does not bubble never reaches the hub — the hub is an ancestor, and that
66
- * is the whole delivery mechanism.
67
- */
68
- export function requests() {
69
- const seen = [];
70
- document.body.addEventListener(LB_EVENT_NAME, (e) => {
71
- seen.push(e.detail);
72
- });
73
- return seen;
74
- }
75
- /** Fire the event a control fires when a person uses it. */
76
- export function change(el) {
77
- el.dispatchEvent(new Event("change", { bubbles: true }));
78
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * lb-input — see docs/testing.md, tier 4.
3
- *
4
- * The simplest widget there is, and therefore the one that states the leaf
5
- * contract on its own: the hub writes one attribute name for every widget
6
- * type, and the widget forwards it to the control it owns.
7
- */
8
- export {};
9
- //# sourceMappingURL=lb-input.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-input.test.d.ts","sourceRoot":"","sources":["../../tests/lb-input.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -1,78 +0,0 @@
1
- /**
2
- * lb-input — see docs/testing.md, tier 4.
3
- *
4
- * The simplest widget there is, and therefore the one that states the leaf
5
- * contract on its own: the hub writes one attribute name for every widget
6
- * type, and the widget forwards it to the control it owns.
7
- */
8
- import assert from "node:assert/strict";
9
- import { describe, it } from "node:test";
10
- import { ATTR_CELL, ATTR_KEY, ATTR_QUERY, ATTR_VALUE } from "../core/lb-constants";
11
- import { captured } from "./helpers/console";
12
- import { change, installWindow, mount, requests } from "./helpers/window";
13
- installWindow();
14
- await import("../widgets/lb-input");
15
- describe("lb-input receiving a value", () => {
16
- it("forwards its value to the input it owns", () => {
17
- const widget = mount(`<lb-input><input /></lb-input>`);
18
- widget.setAttribute(ATTR_VALUE, "Ada");
19
- assert.equal(widget.querySelector("input").value, "Ada");
20
- });
21
- it("takes the value it was already carrying when it upgraded", () => {
22
- // Hydration and refresh are one operation. The browser runs
23
- // attributeChangedCallback for attributes already present when a widget
24
- // upgrades, so a value that landed before the class existed is not lost —
25
- // which is what lets applyData fill a row and then insert it.
26
- const widget = mount(`<lb-input ${ATTR_VALUE}="Ada"><input /></lb-input>`);
27
- assert.equal(widget.querySelector("input").value, "Ada");
28
- });
29
- it("keeps up with a value that changes", () => {
30
- const widget = mount(`<lb-input><input /></lb-input>`);
31
- widget.setAttribute(ATTR_VALUE, "Ada");
32
- widget.setAttribute(ATTR_VALUE, "Grace");
33
- assert.equal(widget.querySelector("input").value, "Grace");
34
- });
35
- it("reports when there is no input to receive the value", async () => {
36
- const widget = mount(`<lb-input></lb-input>`);
37
- const { error } = await captured(() => widget.setAttribute(ATTR_VALUE, "Ada"));
38
- assert.match(error.join("\n"), /lb-input: no <input> to receive/);
39
- });
40
- it("watches only the value attribute", async () => {
41
- // observedAttributes is the whole of what a widget is told about. A page
42
- // author's class or title must not reach the control.
43
- const widget = mount(`<lb-input><input /></lb-input>`);
44
- const { error } = await captured(() => {
45
- widget.setAttribute("class", "wide");
46
- widget.setAttribute("title", "Name");
47
- });
48
- assert.deepEqual(error, []);
49
- assert.equal(widget.querySelector("input").value, "");
50
- });
51
- });
52
- describe("lb-input sending a cell-change", () => {
53
- it("sends its coordinates and the input's current value", () => {
54
- const seen = requests();
55
- const widget = mount(`<div ${ATTR_QUERY}="members" ${ATTR_KEY}="7">` +
56
- `<lb-input ${ATTR_CELL}="name"><input /></lb-input></div>`).querySelector("lb-input");
57
- widget.querySelector("input").value = "Ada";
58
- change(widget.querySelector("input"));
59
- assert.deepEqual(seen, [
60
- { op: "cell-change", query: "members", key: "7", cell: "name", value: "Ada" },
61
- ]);
62
- });
63
- it("reports a change with no query/key/cell coordinates, and sends nothing", async () => {
64
- const seen = requests();
65
- const widget = mount(`<lb-input ${ATTR_CELL}="name"><input /></lb-input>`);
66
- const { error } = await captured(() => change(widget.querySelector("input")));
67
- assert.match(error.join("\n"), /lb-input: change with no query\/key\/cell coordinates/);
68
- assert.deepEqual(seen, []);
69
- });
70
- it("reports a change with no input, and sends nothing", async () => {
71
- const seen = requests();
72
- const widget = mount(`<div ${ATTR_QUERY}="members" ${ATTR_KEY}="7">` +
73
- `<lb-input ${ATTR_CELL}="name"></lb-input></div>`).querySelector("lb-input");
74
- const { error } = await captured(() => change(widget));
75
- assert.match(error.join("\n"), /lb-input: change with no <input>/);
76
- assert.deepEqual(seen, []);
77
- });
78
- });
@@ -1,12 +0,0 @@
1
- /**
2
- * lb-list — see docs/testing.md, tier 4.
3
- *
4
- * The plain repeater: whatever the page author wrote inside is repeated once
5
- * per tuple, in the order the server sent. It adds no placement of its own,
6
- * so what is worth testing is that it is a row host at all, and the case it
7
- * exists for — a <table> that cannot be one, because a custom element written
8
- * inside <tbody> is discarded by the parser. The widget goes around the
9
- * table, and the template goes inside it.
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"}
@@ -1,44 +0,0 @@
1
- /**
2
- * lb-list — see docs/testing.md, tier 4.
3
- *
4
- * The plain repeater: whatever the page author wrote inside is repeated once
5
- * per tuple, in the order the server sent. It adds no placement of its own,
6
- * so what is worth testing is that it is a row host at all, and the case it
7
- * exists for — a <table> that cannot be one, because a custom element written
8
- * inside <tbody> is discarded by the parser. The widget goes around the
9
- * table, and the template goes inside it.
10
- */
11
- import assert from "node:assert/strict";
12
- import { describe, it } from "node:test";
13
- import { installWindow, mount } from "./helpers/window";
14
- installWindow();
15
- await import("../widgets/lb-list");
16
- const rows = (list) => ({ op: "rows", rows: list });
17
- describe("lb-list", () => {
18
- it("repeats what the page wrote, once per tuple", () => {
19
- const widget = mount(`<lb-list><ul><template lb-key="id"><li lb-cell="name"></li></template></ul></lb-list>`);
20
- widget.acceptRows(rows([
21
- { id: "1", name: "Ada" },
22
- { id: "2", name: "Bab" },
23
- ]));
24
- assert.deepEqual([...widget.querySelectorAll("li")].map((li) => li.textContent), ["Ada", "Bab"]);
25
- });
26
- it("goes around a table it cannot be inside", () => {
27
- // The reason this widget is not the <table> itself. Everything the hub
28
- // needs still works: the template is inside <tbody>, where the content
29
- // model already allows it, and the rows land there.
30
- const widget = mount(`<lb-list><table><tbody>` +
31
- `<template lb-key="id"><tr><td lb-cell="name"></td></tr></template>` +
32
- `</tbody></table></lb-list>`);
33
- widget.acceptRows(rows([{ id: "1", name: "Ada" }]));
34
- const row = widget.querySelector("tbody tr");
35
- assert.equal(row.textContent, "Ada");
36
- assert.equal(widget.getAttribute("data-rows"), "1");
37
- });
38
- it("survives the parser dropping a widget written inside a tbody", () => {
39
- // The failure this design avoids, stated as what the parser actually
40
- // does: a lb-list inside <tbody> is not there afterwards.
41
- const table = mount(`<table><tbody><lb-list><template lb-key="id"><tr></tr></template></lb-list></tbody></table>`);
42
- assert.equal(table.querySelector("tbody lb-list"), null);
43
- });
44
- });
@@ -1,10 +0,0 @@
1
- /**
2
- * lb-options — see docs/testing.md, tier 4.
3
- *
4
- * The case that shows why placement belongs to the widget. The rows arrive
5
- * flat and keyed; nothing on the wire knows about <optgroup>, and nothing
6
- * needs to. Turning a repeated column value into an element is exactly the
7
- * local knowledge a widget has and the hub does not.
8
- */
9
- export {};
10
- //# sourceMappingURL=lb-options.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-options.test.d.ts","sourceRoot":"","sources":["../../tests/lb-options.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -1,121 +0,0 @@
1
- /**
2
- * lb-options — see docs/testing.md, tier 4.
3
- *
4
- * The case that shows why placement belongs to the widget. The rows arrive
5
- * flat and keyed; nothing on the wire knows about <optgroup>, and nothing
6
- * needs to. Turning a repeated column value into an element is exactly the
7
- * local knowledge a widget has and the hub does not.
8
- */
9
- import assert from "node:assert/strict";
10
- import { describe, it } from "node:test";
11
- import { ATTR_ACTION } from "../core/lb-constants";
12
- import { change, installWindow, mount, requests } from "./helpers/window";
13
- installWindow();
14
- await import("../widgets/lb-options");
15
- const ROW = `<option lb-cell="name"></option>`;
16
- /** A lb-options with the row template a page author would write. */
17
- function options(template) {
18
- return mount(`<lb-options ${ATTR_ACTION}="pick"><label><select>${template}</select></label></lb-options>`);
19
- }
20
- const flat = `<template lb-key="id">${ROW}</template>`;
21
- const grouped = `<template lb-key="id" lb-group="team">${ROW}</template>`;
22
- const rows = (list) => ({ op: "rows", rows: list });
23
- const patch = (change) => ({ op: "patch", ...change });
24
- /** Every option showing, as label and value. */
25
- function shown(widget) {
26
- return [...widget.querySelectorAll("option")].map((o) => [
27
- o.textContent,
28
- o.value,
29
- ]);
30
- }
31
- /** The groups showing, as label and the options in each. */
32
- function groups(widget) {
33
- return [...widget.querySelectorAll("optgroup")].map((g) => [
34
- g.label,
35
- [...g.querySelectorAll("option")].map((o) => o.textContent),
36
- ]);
37
- }
38
- describe("lb-options without grouping", () => {
39
- it("lands one option per tuple, in the order sent", () => {
40
- const widget = options(flat);
41
- widget.acceptRows(rows([
42
- { id: "1", name: "Ada" },
43
- { id: "2", name: "Bab" },
44
- ]));
45
- assert.deepEqual(shown(widget), [
46
- ["Ada", "1"],
47
- ["Bab", "2"],
48
- ]);
49
- });
50
- it("makes the key the value, so the page says it once", () => {
51
- // The identity of the row is the value of the option.
52
- const widget = options(flat);
53
- widget.acceptRows(rows([{ id: "7", name: "Ada" }]));
54
- const option = widget.querySelector("option");
55
- assert.equal(option.value, "7");
56
- assert.equal(option.getAttribute("lb-key"), "7");
57
- });
58
- it("puts the options inside the select, not beside it", () => {
59
- const widget = options(flat);
60
- widget.acceptRows(rows([{ id: "1", name: "Ada" }]));
61
- assert.equal(widget.querySelector("option").parentElement.localName, "select");
62
- });
63
- });
64
- describe("lb-options grouping", () => {
65
- it("builds one optgroup per distinct value of the grouping column", () => {
66
- const widget = options(grouped);
67
- widget.acceptRows(rows([
68
- { id: "1", name: "Ada", team: "Core" },
69
- { id: "2", name: "Bab", team: "Ops" },
70
- { id: "3", name: "Cai", team: "Core" },
71
- ]));
72
- assert.deepEqual(groups(widget), [
73
- ["Core", ["Ada", "Cai"]],
74
- ["Ops", ["Bab"]],
75
- ]);
76
- });
77
- it("removes a group when its last row goes", () => {
78
- // A group is derived, so it goes when the thing it was derived from does.
79
- const widget = options(grouped);
80
- widget.acceptRows(rows([
81
- { id: "1", name: "Ada", team: "Core" },
82
- { id: "2", name: "Bab", team: "Ops" },
83
- ]));
84
- widget.acceptRows(rows([{ id: "1", name: "Ada", team: "Core" }]));
85
- assert.deepEqual(groups(widget), [["Core", ["Ada"]]]);
86
- });
87
- it("removes a group emptied by a patch", () => {
88
- const widget = options(grouped);
89
- widget.acceptRows(rows([
90
- { id: "1", name: "Ada", team: "Core" },
91
- { id: "2", name: "Bab", team: "Ops" },
92
- ]));
93
- widget.acceptRows(patch({ drop: ["2"] }));
94
- assert.deepEqual(groups(widget), [["Core", ["Ada"]]]);
95
- });
96
- it("groups a tuple with no value for the column under the empty label", () => {
97
- const widget = options(grouped);
98
- widget.acceptRows(rows([{ id: "1", name: "Ada" }]));
99
- assert.deepEqual(groups(widget), [["", ["Ada"]]]);
100
- });
101
- it("keeps the template as the marker the groups sit in front of", () => {
102
- const widget = options(grouped);
103
- widget.acceptRows(rows([{ id: "1", name: "Ada", team: "Core" }]));
104
- const select = widget.querySelector("select");
105
- assert.equal(select.lastElementChild.localName, "template");
106
- });
107
- });
108
- describe("lb-options sending an action", () => {
109
- it("sends the declared action carrying the choice", () => {
110
- const seen = requests();
111
- const widget = options(flat);
112
- widget.acceptRows(rows([
113
- { id: "1", name: "Ada" },
114
- { id: "2", name: "Bab" },
115
- ]));
116
- const select = widget.querySelector("select");
117
- select.value = "2";
118
- change(select);
119
- assert.deepEqual(seen, [{ op: "action", name: "pick", value: "2" }]);
120
- });
121
- });
@@ -1,14 +0,0 @@
1
- /**
2
- * lb-picker — see docs/testing.md, tier 4.
3
- *
4
- * The same select as lb-options, with the row template supplied by the
5
- * definition instead of by the page. The behavior is inherited whole, because
6
- * it is the same behavior; a distinct class exists only because
7
- * customElements.define requires one constructor per name.
8
- *
9
- * So what is worth testing is exactly that: that nothing was written twice,
10
- * and that the expanded definition drives the inherited machinery. The
11
- * template below is what widgets/lb-picker.html expands to.
12
- */
13
- export {};
14
- //# sourceMappingURL=lb-picker.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-picker.test.d.ts","sourceRoot":"","sources":["../../tests/lb-picker.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -1,59 +0,0 @@
1
- /**
2
- * lb-picker — see docs/testing.md, tier 4.
3
- *
4
- * The same select as lb-options, with the row template supplied by the
5
- * definition instead of by the page. The behavior is inherited whole, because
6
- * it is the same behavior; a distinct class exists only because
7
- * customElements.define requires one constructor per name.
8
- *
9
- * So what is worth testing is exactly that: that nothing was written twice,
10
- * and that the expanded definition drives the inherited machinery. The
11
- * template below is what widgets/lb-picker.html expands to.
12
- */
13
- import assert from "node:assert/strict";
14
- import { describe, it } from "node:test";
15
- import { ATTR_ACTION } from "../core/lb-constants";
16
- import { change, installWindow, mount, requests } from "./helpers/window";
17
- installWindow();
18
- const { LbOptions } = await import("../widgets/lb-options");
19
- await import("../widgets/lb-picker");
20
- /** lb-picker as expansion leaves it, given exp-key, exp-cell and exp-group. */
21
- function picker() {
22
- return mount(`<lb-picker ${ATTR_ACTION}="pick"><label>Member:` +
23
- `<select><template lb-key="id" lb-group="team">` +
24
- `<option lb-cell="name"></option>` +
25
- `</template></select></label></lb-picker>`);
26
- }
27
- const rows = (list) => ({
28
- op: "rows",
29
- rows: list,
30
- });
31
- describe("lb-picker", () => {
32
- it("is a lb-options", () => {
33
- assert.ok(picker() instanceof LbOptions);
34
- });
35
- it("lands rows in groups, from the template its definition supplied", () => {
36
- const widget = picker();
37
- widget.acceptRows(rows([
38
- { id: "1", name: "Ada", team: "Core" },
39
- { id: "2", name: "Bab", team: "Ops" },
40
- ]));
41
- assert.deepEqual([...widget.querySelectorAll("optgroup")].map((g) => g.label), ["Core", "Ops"]);
42
- assert.deepEqual([...widget.querySelectorAll("option")].map((o) => [
43
- o.textContent,
44
- o.value,
45
- ]), [
46
- ["Ada", "1"],
47
- ["Bab", "2"],
48
- ]);
49
- });
50
- it("sends the declared action on a change", () => {
51
- const seen = requests();
52
- const widget = picker();
53
- widget.acceptRows(rows([{ id: "1", name: "Ada", team: "Core" }]));
54
- const select = widget.querySelector("select");
55
- select.value = "1";
56
- change(select);
57
- assert.deepEqual(seen, [{ op: "action", name: "pick", value: "1" }]);
58
- });
59
- });
@@ -1,9 +0,0 @@
1
- /**
2
- * lb-select — see docs/testing.md, tier 4.
3
- *
4
- * The request vocabulary and the addressing vocabulary running in both
5
- * directions on one widget: it receives its value like any other cell, and on
6
- * change it sends the action the page declared for it.
7
- */
8
- export {};
9
- //# sourceMappingURL=lb-select.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-select.test.d.ts","sourceRoot":"","sources":["../../tests/lb-select.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -1,71 +0,0 @@
1
- /**
2
- * lb-select — see docs/testing.md, tier 4.
3
- *
4
- * The request vocabulary and the addressing vocabulary running in both
5
- * directions on one widget: it receives its value like any other cell, and on
6
- * change it sends the action the page declared for it.
7
- */
8
- import assert from "node:assert/strict";
9
- import { describe, it } from "node:test";
10
- import { ATTR_ACTION, ATTR_VALUE } from "../core/lb-constants";
11
- import { captured } from "./helpers/console";
12
- import { change, installWindow, mount, requests } from "./helpers/window";
13
- installWindow();
14
- await import("../widgets/lb-select");
15
- const OPTIONS = `<option value="lead">Lead</option><option value="staff">Staff</option>`;
16
- describe("lb-select receiving a value", () => {
17
- it("forwards it to the select it owns", () => {
18
- const widget = mount(`<lb-select><select>${OPTIONS}</select></lb-select>`);
19
- widget.setAttribute(ATTR_VALUE, "staff");
20
- assert.equal(widget.querySelector("select").value, "staff");
21
- });
22
- it("takes the value it was already carrying when it upgraded", () => {
23
- const widget = mount(`<lb-select ${ATTR_VALUE}="staff"><select>${OPTIONS}</select></lb-select>`);
24
- assert.equal(widget.querySelector("select").value, "staff");
25
- });
26
- it("reports when there is no select to receive it", async () => {
27
- const widget = mount(`<lb-select></lb-select>`);
28
- const { error } = await captured(() => widget.setAttribute(ATTR_VALUE, "staff"));
29
- assert.match(error.join("\n"), /lb-select: no <select> to receive/);
30
- });
31
- });
32
- describe("lb-select sending an action", () => {
33
- it("sends the declared action carrying the choice", () => {
34
- // The choice is the interaction, so this is the case where an action
35
- // carries a value. The server still decides what that choice means.
36
- const seen = requests();
37
- const widget = mount(`<lb-select ${ATTR_ACTION}="setRole"><select>${OPTIONS}</select></lb-select>`);
38
- const select = widget.querySelector("select");
39
- select.value = "staff";
40
- change(select);
41
- assert.deepEqual(seen, [{ op: "action", name: "setRole", value: "staff" }]);
42
- });
43
- it("carries no argument list beyond that one value", () => {
44
- // What keeps this from being an RPC endpoint: a name the page declared,
45
- // and at most the one value a control has.
46
- const seen = requests();
47
- const widget = mount(`<lb-select ${ATTR_ACTION}="setRole"><select>${OPTIONS}</select></lb-select>`);
48
- change(widget.querySelector("select"));
49
- assert.deepEqual(Object.keys(seen[0]), ["op", "name", "value"]);
50
- });
51
- it("sends an event that bubbles, because the hub is an ancestor", () => {
52
- const seen = requests();
53
- const widget = mount(`<lb-select ${ATTR_ACTION}="setRole"><select>${OPTIONS}</select></lb-select>`);
54
- change(widget.querySelector("select"));
55
- assert.equal(seen.length, 1, "the request reached the body");
56
- });
57
- it("reports a change it has no action for, and sends nothing", async () => {
58
- const seen = requests();
59
- const widget = mount(`<lb-select><select>${OPTIONS}</select></lb-select>`);
60
- const { error } = await captured(() => change(widget.querySelector("select")));
61
- assert.match(error.join("\n"), /lb-select: no lb-action/);
62
- assert.deepEqual(seen, []);
63
- });
64
- it("reports a change with no select, and sends nothing", async () => {
65
- const seen = requests();
66
- const widget = mount(`<lb-select ${ATTR_ACTION}="setRole"></lb-select>`);
67
- const { error } = await captured(() => change(widget));
68
- assert.match(error.join("\n"), /lb-select: change with no <select>/);
69
- assert.deepEqual(seen, []);
70
- });
71
- });