@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,226 @@
1
+ /**
2
+ * Expansion — see docs/expansion.md and docs/testing.md, tier 1.
3
+ *
4
+ * Definitions are written inline here because `expand` takes them as a map
5
+ * and a two-line definition beside its assertion reads better than a file
6
+ * reference. The cases that genuinely need files — the checks `loadDefinitions`
7
+ * runs, and directory ordering — use tests/fixtures/definitions.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { describe, it } from "node:test";
11
+ import { fileURLToPath } from "node:url";
12
+ import { JSDOM } from "jsdom";
13
+ import { expand, loadDefinitions } from "../build/expand";
14
+ const FIXTURES = fileURLToPath(new URL("./fixtures/definitions", import.meta.url));
15
+ /**
16
+ * Read expansion's output as a tree.
17
+ *
18
+ * Used where the claim is about the document rather than about the text —
19
+ * which elements exist, which attributes an element has. Matching a
20
+ * serialization would test the serializer.
21
+ */
22
+ function parseHtml(html) {
23
+ const holder = new JSDOM("").window.document.createElement("div");
24
+ holder.innerHTML = html;
25
+ return holder;
26
+ }
27
+ /** Definitions are keyed by the uppercased tag they define. */
28
+ function defs(entries) {
29
+ const map = {};
30
+ for (const [tag, source] of Object.entries(entries)) {
31
+ map[tag.toUpperCase()] = source;
32
+ }
33
+ return map;
34
+ }
35
+ // --- Rejections ----------------------------------------------------------
36
+ //
37
+ // Expansion runs before any request exists, so every one of these is a build
38
+ // that must not produce output. Each asserts on the message as well as the
39
+ // throw: an author reads the message, and it is the whole of the diagnosis.
40
+ describe("expansion rejects", () => {
41
+ it("a hub tag with no definition", () => {
42
+ assert.throws(() => expand(`<lb-nope></lb-nope>`, defs({})), /<lb-nope> has no definition/);
43
+ });
44
+ it("a parameter the definition never declared", () => {
45
+ assert.throws(() => expand(`<lb-thing exp-label="Name"></lb-thing>`, defs({ "lb-thing": `<div></div>` })), /given exp-label, but its definition has no \{\{label\}\}/);
46
+ });
47
+ it("a placeholder no tag could supply", async () => {
48
+ await assert.rejects(() => loadDefinitions([`${FIXTURES}/camel`]), /declares \{\{inputClass\}\}.*Write \{\{input-class\}\}/s);
49
+ });
50
+ it("a cycle in the definition graph", async () => {
51
+ await assert.rejects(() => loadDefinitions([`${FIXTURES}/cycle`]), /cycle in the definition graph/);
52
+ });
53
+ it("two destinations with one name", () => {
54
+ assert.throws(() => expand(`<lb-thing></lb-thing>`, defs({
55
+ "lb-thing": `<table><thead lb-template="head"></thead><tfoot lb-template="head"></tfoot></table>`,
56
+ })), /declares two lb-template="head" destinations/);
57
+ });
58
+ it("two templates for one destination", () => {
59
+ assert.throws(() => expand(`<lb-thing>` +
60
+ `<template lb-template="head"><tr></tr></template>` +
61
+ `<template lb-template="head"><tr></tr></template>` +
62
+ `</lb-thing>`, defs({
63
+ "lb-thing": `<table><thead lb-template="head"></thead></table>`,
64
+ })), /was given two templates for 'head'/);
65
+ });
66
+ it("a template naming a destination the definition does not have", () => {
67
+ assert.throws(() => expand(`<lb-thing><template lb-template="foot"><tr></tr></template></lb-thing>`, defs({
68
+ "lb-thing": `<table><thead lb-template="head"></thead></table>`,
69
+ })), /has no destination by that name/);
70
+ });
71
+ it("content written into a definition with no slot", () => {
72
+ assert.throws(() => expand(`<lb-thing><p>hello</p></lb-thing>`, defs({ "lb-thing": `<div></div>` })), /content was written inside <lb-thing>, whose definition has no lb-slot/);
73
+ });
74
+ it("a definition declaring more than one slot", () => {
75
+ assert.throws(() => expand(`<lb-thing></lb-thing>`, defs({
76
+ "lb-thing": `<div lb-slot></div><span lb-slot></span>`,
77
+ })), /declares more than one lb-slot/);
78
+ });
79
+ });
80
+ // --- Substitution --------------------------------------------------------
81
+ describe("substitution", () => {
82
+ it("fills an attribute and a text node", () => {
83
+ const html = expand(`<lb-thing exp-label="Name" exp-name="who"></lb-thing>`, defs({
84
+ "lb-thing": `<label>{{label}}<input name="{{name}}" /></label>`,
85
+ }));
86
+ assert.match(html, /<label>Name<input name="who"><\/label>/);
87
+ });
88
+ it("drops an attribute whose placeholder was not supplied", () => {
89
+ // Presence propagates: an unsupplied placeholder must not emit
90
+ // readonly="", which HTML reads as readonly.
91
+ const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<input readonly="{{readonly}}" />` }));
92
+ assert.match(html, /<input>/);
93
+ assert.doesNotMatch(html, /readonly/);
94
+ });
95
+ it("keeps the whitespace around a text placeholder", () => {
96
+ // "{{label}} <input>" has to keep separating the label from the control.
97
+ const html = expand(`<lb-thing exp-label="Name"></lb-thing>`, defs({ "lb-thing": `<label>{{label}} <input /></label>` }));
98
+ assert.match(html, /<label>Name <input><\/label>/);
99
+ });
100
+ it("fills an unsupplied text placeholder with nothing", () => {
101
+ const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<label>{{label}}</label>` }));
102
+ assert.match(html, /<label><\/label>/);
103
+ });
104
+ it("reaches placeholders inside template content", () => {
105
+ // lb-picker ships a row template of its own, so {{key}} written inside
106
+ // one has to be a placeholder rather than literal text that ships.
107
+ const html = expand(`<lb-thing exp-key="id" exp-cell="name"></lb-thing>`, defs({
108
+ "lb-thing": `<select><template lb-key="{{key}}"><option lb-cell="{{cell}}"></option></template></select>`,
109
+ }));
110
+ assert.match(html, /<template lb-key="id">/);
111
+ assert.match(html, /<option lb-cell="name">/);
112
+ });
113
+ it("leaves a partial placeholder alone", () => {
114
+ // A placeholder is an entire attribute value or an entire text node.
115
+ const html = expand(`<lb-thing></lb-thing>`, defs({ "lb-thing": `<label title="a {{label}} b"></label>` }));
116
+ assert.match(html, /title="a \{\{label\}\} b"/);
117
+ });
118
+ it("refuses a parameter for a partial placeholder", () => {
119
+ // The consequence of the rule above, and the reason it is safe: a name
120
+ // that is not a placeholder is a name the definition never declared, so
121
+ // supplying it is the same error as supplying a name that is not there
122
+ // at all. The author is told rather than shipped literal braces.
123
+ assert.throws(() => expand(`<lb-thing exp-label="Name"></lb-thing>`, defs({ "lb-thing": `<label title="a {{label}} b"></label>` })), /has no \{\{label\}\}/);
124
+ });
125
+ it("keeps the exp- attributes on the expanded tag", () => {
126
+ // What ships shows what was asked for beside what it produced.
127
+ const html = expand(`<lb-thing exp-label="Name"></lb-thing>`, defs({ "lb-thing": `<label>{{label}}</label>` }));
128
+ assert.match(html, /<lb-thing exp-label="Name">/);
129
+ });
130
+ it("cannot inject markup, by construction", () => {
131
+ // Substitution goes through setAttribute and node values, never through a
132
+ // string. This test exists to fail if anyone reaches for concatenation.
133
+ //
134
+ // The assertion reparses rather than matching the serialization, because
135
+ // the serialization is not the claim. An expanded tag keeps the exp-
136
+ // attributes it was given, and `<` in an attribute value is legal HTML
137
+ // that no serializer has to escape — so the output does contain the
138
+ // characters. What must not exist is an element.
139
+ const html = expand(`<lb-thing exp-label="<img src=x onerror=alert(1)>" exp-name="&quot; onclick=&quot;alert(1)"></lb-thing>`, defs({
140
+ "lb-thing": `<label>{{label}}<input name="{{name}}" /></label>`,
141
+ }));
142
+ const tree = parseHtml(html);
143
+ assert.equal(tree.querySelectorAll("img").length, 0);
144
+ assert.equal(tree.querySelector("label").textContent, "<img src=x onerror=alert(1)>");
145
+ // The second value closes an attribute and opens an event handler, if it
146
+ // is ever concatenated. Through setAttribute it is one attribute's value.
147
+ const input = tree.querySelector("input");
148
+ assert.deepEqual([...input.attributes].map((a) => a.name), ["name"]);
149
+ assert.equal(input.getAttribute("name"), `" onclick="alert(1)`);
150
+ });
151
+ });
152
+ // --- Slots and destinations ---------------------------------------------
153
+ describe("placement", () => {
154
+ it("puts authored content in the slot and removes the marker", () => {
155
+ const html = expand(`<lb-list><p>hello</p></lb-list>`, defs({ "lb-list": `<div lb-slot></div>` }));
156
+ assert.match(html, /<div><p>hello<\/p><\/div>/);
157
+ assert.doesNotMatch(html, /lb-slot/);
158
+ });
159
+ it("unwraps a named template into its destination as real markup", () => {
160
+ // The wrapper existed only to survive the parser. What ships is a real
161
+ // <tr> in a real <thead>, visible in view-source like everything else.
162
+ const html = expand(`<lb-table>` +
163
+ `<template lb-template="head"><tr><th>Name</th></tr></template>` +
164
+ `<template lb-key="id"><tr><td lb-cell="name"></td></tr></template>` +
165
+ `</lb-table>`, defs({
166
+ "lb-table": `<table><thead lb-template="head"></thead><tbody lb-slot></tbody></table>`,
167
+ }));
168
+ assert.match(html, /<thead><tr><th>Name<\/th><\/tr><\/thead>/);
169
+ assert.match(html, /<tbody><template lb-key="id">/);
170
+ assert.doesNotMatch(html, /lb-template/);
171
+ });
172
+ it("does not count whitespace as content", () => {
173
+ // An author who writes only named templates still leaves the newlines
174
+ // between them, and those are the file's formatting.
175
+ const html = expand(`<lb-thing>\n \n</lb-thing>`, defs({ "lb-thing": `<div></div>` }));
176
+ assert.match(html, /<lb-thing><div><\/div><\/lb-thing>/);
177
+ });
178
+ it("expands a widget used inside a definition", () => {
179
+ const html = expand(`<lb-outer></lb-outer>`, defs({
180
+ "lb-outer": `<div><lb-inner exp-label="deep"></lb-inner></div>`,
181
+ "lb-inner": `<span>{{label}}</span>`,
182
+ }));
183
+ assert.match(html, /<span>deep<\/span>/);
184
+ });
185
+ it("expands a widget written inside a page's row template", () => {
186
+ // Template content is markup in a file like any other, and it is inert,
187
+ // so a row template ships already expanded.
188
+ const html = expand(`<lb-list><template lb-key="id"><lb-inner exp-label="row"></lb-inner></template></lb-list>`, defs({
189
+ "lb-list": `<div lb-slot></div>`,
190
+ "lb-inner": `<span>{{label}}</span>`,
191
+ }));
192
+ assert.match(html, /<span>row<\/span>/);
193
+ });
194
+ it("expands a tag outside the lb- namespace when it has a definition", () => {
195
+ // An application's widget can use its own prefix — see docs/getting-started.md.
196
+ // The definition must still apply; discovering it is not enough.
197
+ const html = expand(`<app-nav exp-label="Site"></app-nav>`, defs({ "app-nav": `<nav aria-label="{{label}}" lb-slot></nav>` }));
198
+ assert.match(html, /<nav aria-label="Site"><\/nav>/);
199
+ });
200
+ it("leaves a tag outside the lb- namespace alone when it has no definition", () => {
201
+ // A plain custom element, registered by script alone — see build/elements.ts.
202
+ const html = expand(`<app-box>hi</app-box>`, defs({}));
203
+ assert.match(html, /<app-box>hi<\/app-box>/);
204
+ });
205
+ });
206
+ // --- Loading -------------------------------------------------------------
207
+ describe("loadDefinitions", () => {
208
+ it("keys a definition by the tag its filename names", async () => {
209
+ const loaded = await loadDefinitions([`${FIXTURES}/base`]);
210
+ assert.deepEqual(Object.keys(loaded), ["LB-THING"]);
211
+ });
212
+ it("lets a later directory override a built-in", async () => {
213
+ const loaded = await loadDefinitions([
214
+ `${FIXTURES}/base`,
215
+ `${FIXTURES}/override`,
216
+ ]);
217
+ assert.match(loaded["LB-THING"], /application/);
218
+ });
219
+ it("skips a directory that is not there", async () => {
220
+ const loaded = await loadDefinitions([
221
+ `${FIXTURES}/nothing-here`,
222
+ `${FIXTURES}/base`,
223
+ ]);
224
+ assert.deepEqual(Object.keys(loaded), ["LB-THING"]);
225
+ });
226
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ "acme-widget": string;
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/collision/elements.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
@@ -0,0 +1,3 @@
1
+ export default {
2
+ "acme-widget": "@acme/widget",
3
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acme-widget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acme-widget.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/collision/widgets/acme-widget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=app-box.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-box.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/local/widgets/app-box.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ "acme-widget": string;
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest/elements.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
@@ -0,0 +1,3 @@
1
+ export default {
2
+ "acme-widget": "@acme/widget",
3
+ };
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ acmewidget: string;
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-bad-tag/elements.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
@@ -0,0 +1,3 @@
1
+ export default {
2
+ acmewidget: "@acme/widget",
3
+ };
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ "acme-widget": number;
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-bad-value/elements.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
@@ -0,0 +1,3 @@
1
+ export default {
2
+ "acme-widget": 42,
3
+ };
@@ -0,0 +1,19 @@
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
+ export {};
19
+ //# sourceMappingURL=golden.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"golden.test.d.ts","sourceRoot":"","sources":["../../tests/golden.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
@@ -0,0 +1,60 @@
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
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The console is an interface — see docs/testing.md.
3
+ *
4
+ * Loadbare App reports what it survives through `console.warn` and `console.error`:
5
+ * a page that does not exist, a query with no scope, a list widget with no
6
+ * template. The behavior under test in those cases is that Loadbare App reported and
7
+ * carried on, so the report is as much the assertion as the return value is.
8
+ *
9
+ * This is the seam until those calls go through one `report()` in `core/`.
10
+ */
11
+ type Line = string;
12
+ export interface Captured<T> {
13
+ result: T;
14
+ warn: Line[];
15
+ error: Line[];
16
+ }
17
+ /** Run something with the console collected rather than printed. */
18
+ export declare function captured<T>(run: () => T | Promise<T>): Promise<Captured<T>>;
19
+ export {};
20
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../tests/helpers/console.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,KAAK,IAAI,GAAG,MAAM,CAAC;AAEnB,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,CAAC;IACV,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,oEAAoE;AACpE,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,GAAG,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAgBtB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The console is an interface — see docs/testing.md.
3
+ *
4
+ * Loadbare App reports what it survives through `console.warn` and `console.error`:
5
+ * a page that does not exist, a query with no scope, a list widget with no
6
+ * template. The behavior under test in those cases is that Loadbare App reported and
7
+ * carried on, so the report is as much the assertion as the return value is.
8
+ *
9
+ * This is the seam until those calls go through one `report()` in `core/`.
10
+ */
11
+ /** Run something with the console collected rather than printed. */
12
+ export async function captured(run) {
13
+ const warn = [];
14
+ const error = [];
15
+ const realWarn = console.warn;
16
+ const realError = console.error;
17
+ // Arguments are joined the way the console would show them, so a test can
18
+ // match one string against the message an author would read.
19
+ console.warn = (...args) => void warn.push(args.join(" "));
20
+ console.error = (...args) => void error.push(args.join(" "));
21
+ try {
22
+ return { result: await run(), warn, error };
23
+ }
24
+ finally {
25
+ console.warn = realWarn;
26
+ console.error = realError;
27
+ }
28
+ }
@@ -0,0 +1,18 @@
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
+ /** Parse markup and hand back the body holding it. */
15
+ export declare function body(html: string): HTMLElement;
16
+ /** Parse markup and hand back its first element. */
17
+ export declare function one(html: string): Element;
18
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../../tests/helpers/dom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,sDAAsD;AACtD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAE9C;AAED,oDAAoD;AACpD,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzC"}
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,43 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,8 @@
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-apply.test.d.ts","sourceRoot":"","sources":["../../tests/lb-apply.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}