@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
@@ -0,0 +1,163 @@
1
+ # The Basic Widget Library
2
+
3
+ Six widgets: `lb-input`, `lb-select`, `lb-list`, `lb-options`, `lb-table`,
4
+ `lb-picker`. Every one of them is written against the same two contracts
5
+ documented elsewhere — [Custom Elements](./custom-elements.md#html) for its
6
+ definition, [Custom Elements](./custom-elements.md#code) for its class —
7
+ nothing here is special-cased machinery.
8
+
9
+ They ship compiled, in `@loadbare/widgets`, a package the builder resolves the
10
+ way it resolves anyone else's — it declares `"loadbare": { "widgets": "./dist" }`
11
+ and the builder scans that. Install it and list it:
12
+
13
+ ```
14
+ npm install @loadbare/widgets
15
+ ```
16
+
17
+ ```ts
18
+ // src/imports.ts
19
+ export default ["@loadbare/widgets"];
20
+ ```
21
+
22
+ See [Using Widget Libraries](../tutorials/090-using-widget-libraries.md) for
23
+ what listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
24
+ for where a listed package sits in the cascade.
25
+
26
+ ## `lb-input`
27
+
28
+ Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
29
+ nothing on its own: `data-fire-on-change` asks it to send `cell-change` on the
30
+ input's `change`, addressed by its own `lb-query`/`lb-key`/`lb-cell`
31
+ coordinates.
32
+
33
+ An input inside an `lb-insert` or `lb-update` form leaves the attribute off.
34
+ The form reads every `lb-cell` in it on submit and sends one request for all
35
+ of them, so an input that also sent its own would write the same edit twice.
36
+
37
+ | Parameter | Fills |
38
+ | ---------- | ----- |
39
+ | `exp-label` | the visible `<label>` text |
40
+ | `exp-readonly` | the input's `readonly` attribute |
41
+
42
+ | Attribute | Asks for |
43
+ | ---------- | ----- |
44
+ | `data-fire-on-change` | an edit to be sent, on `change` |
45
+
46
+ ## `lb-select`
47
+
48
+ Wraps a `<select>` whose `<option>`s the author writes directly inside (via
49
+ `lb-slot`). `lb-value` sets the select's `.value`; a `change` sends the
50
+ action named by `lb-action`, with the select's `.value` as the request's
51
+ `value` — the choice is the interaction, so this is the case where an
52
+ action carries a value.
53
+
54
+ | Parameter | Fills |
55
+ | ---------- | ----- |
56
+ | `exp-label` | the visible `<label>` text |
57
+
58
+ Requires `lb-action` — a change with none logs and sends nothing.
59
+
60
+ ## `lb-list`
61
+
62
+ The plain repeater: whatever the author writes inside a
63
+ `<template lb-key="...">` is cloned once per row, in arrival order. No
64
+ grouping, no sorting, no request of its own — it exists because a
65
+ `Projection` has to land on something with `acceptRows`, and a bare
66
+ `<table>` can't be one (a custom element written inside `<tbody>` is
67
+ discarded by the parser).
68
+
69
+ ## `lb-options`
70
+
71
+ A `<select>` whose `<option>`s come from a query instead of being written by
72
+ hand. The author supplies the row template inside the widget (via
73
+ `lb-slot`), same as any list widget:
74
+
75
+ ```html
76
+ <lb-options lb-query="statuses" exp-label="Status" lb-action="setStatus">
77
+ <template lb-key="id" lb-group="category">
78
+ <option lb-cell="label"></option>
79
+ </template>
80
+ </lb-options>
81
+ ```
82
+
83
+ | Parameter | Fills |
84
+ | ---------- | ----- |
85
+ | `exp-label` | the visible `<label>` text |
86
+
87
+ - The row's key becomes the option's `value` — `lb-key` supplies both, so
88
+ nothing is declared twice.
89
+ - `lb-group` on the row template sections the options into `<optgroup>`s,
90
+ one per distinct value, created and removed as rows arrive and leave.
91
+ - `lb-sort` is not read by this widget.
92
+ - A `change` sends the action named by `lb-action`, value from the
93
+ select's `.value`.
94
+
95
+ `lb-picker` is this same class with its row template supplied by the
96
+ definition instead of the page — see below.
97
+
98
+ ## `lb-table`
99
+
100
+ A `<table>` that supplies its own scaffolding; the author supplies the
101
+ heading row, the row template, and optionally a footer, each as a
102
+ `<template>` matched to a destination:
103
+
104
+ ```html
105
+ <lb-table lb-query="ledger" exp-caption="Ledger">
106
+ <template lb-template="head">
107
+ <tr><th>Date</th><th>Amount</th></tr>
108
+ </template>
109
+ <template lb-key="id" lb-sort="date" lb-group="month">
110
+ <tr><td lb-cell="date"></td><td lb-cell="amount"></td></tr>
111
+ </template>
112
+ <template lb-template="foot">
113
+ <tr lb-query="ledger-total"><td>Total</td><td lb-cell="total"></td></tr>
114
+ </template>
115
+ </lb-table>
116
+ ```
117
+
118
+ | Parameter | Fills |
119
+ | ---------- | ----- |
120
+ | `exp-caption` | the `<caption>` text |
121
+
122
+ | Destination | Fills |
123
+ | ------------ | ----- |
124
+ | `head` (`lb-template="head"`) | the `<thead>` content |
125
+ | `foot` (`lb-template="foot"`) | the `<tfoot>` content |
126
+ | slot (no `lb-template`) | the row template, via `lb-slot` on `<tbody>` |
127
+
128
+ - `lb-group` on the row template sections rows under a derived heading row,
129
+ one per distinct value, whose `colSpan` matches the row's own column
130
+ count. `lb-sort` orders rows within a section (or the whole body, with no
131
+ grouping) by comparing each row's cell text.
132
+ - The `foot` destination is not delivered through `acceptRows` — it's an
133
+ ordinary scope carrying its own `lb-query`, resolved by name like any
134
+ other on the page. A grand total is a second projection of the same
135
+ data, not a row the hub hands the table.
136
+
137
+ ## `lb-picker`
138
+
139
+ `lb-options`, with the row template supplied by the definition instead of
140
+ the page — for when every row is one option and nothing else varies:
141
+
142
+ ```html
143
+ <lb-picker
144
+ lb-query="statuses"
145
+ exp-label="Status"
146
+ exp-key="id"
147
+ exp-cell="label"
148
+ exp-group="category"
149
+ lb-action="setStatus"
150
+ ></lb-picker>
151
+ ```
152
+
153
+ | Parameter | Fills |
154
+ | ---------- | ----- |
155
+ | `exp-label` | the visible `<label>` text |
156
+ | `exp-key` | the row template's `lb-key` |
157
+ | `exp-cell` | the option's `lb-cell` |
158
+ | `exp-group` | the row template's `lb-group` |
159
+
160
+ Behavior — grouping, key-as-value, the action on change — is inherited
161
+ whole from `lb-options`; a page author who needs a second element in the
162
+ row, or an option built from two columns, writes `lb-options` and its own
163
+ `<template>` instead.
@@ -0,0 +1,130 @@
1
+ # Roadmap
2
+
3
+ Release 1.0 is optimized to prove that a Loadbare app can be robust and
4
+ highly performant with a codebase that has very low accidental complexity.
5
+
6
+ What follows is everything not yet decided or not yet built: release
7
+ candidates, risks not worth solving speculatively, and open design
8
+ questions. None of it is current behavior — for that, see
9
+ [Reference](./reference/overview.md).
10
+
11
+ ## Open questions in the 1.0 mechanism
12
+
13
+ These are not new scope — they're gaps deliberately left unresolved in the
14
+ mechanism Release 1.0 already ships. Each is here because deciding it
15
+ speculatively, before a real page forces the question, risks designing the
16
+ wrong thing. Revisit when the described symptom actually shows up.
17
+
18
+ ### Staleness and concurrent writers
19
+
20
+ Two tabs, or two users, updating the same projection at once. A solo
21
+ developer testing in one browser will not produce this by accident, and
22
+ retrofitting a version or conflict check onto every tuple after the fact
23
+ touches every widget that writes.
24
+
25
+ ### Nesting
26
+
27
+ Whether a tuple may contain a projection (master-detail, an expanding
28
+ row). [Theory](./theory.md) already flags this as possibly load-bearing if
29
+ disallowed. Worth a decision-in-principle the first time a master-detail
30
+ page is built, even before the mechanism is needed elsewhere.
31
+
32
+ ### Whether `lb-query` may be inherited
33
+
34
+ Currently every scope states its own `lb-query`; nothing resolves one from
35
+ an ancestor. Inheritance would be friendlier to the page author but adds a
36
+ resolution rule, and a resolution rule is a mechanism this framework has
37
+ otherwise avoided. Worth deciding before an application grows deep enough
38
+ nesting that restating the query on every level starts to hurt.
39
+
40
+ ### Whether `lb-key` is always required
41
+
42
+ Undecided whether every row needs `lb-key`, or only a row something
43
+ targets (a delete button, an update form). Revisit if a list widget shows
44
+ up that never needs to address an individual row by key.
45
+
46
+ ### Pending appearance
47
+
48
+ A value that hasn't arrived yet is probably derivable from an absent
49
+ `lb-value` rather than needing a signal of its own. Not yet needed because
50
+ nothing currently produces that gap in practice — revisit if one does.
51
+
52
+ ### Validation placement
53
+
54
+ Per-keystroke feedback cannot afford a round trip, so some validation will
55
+ end up living in the widget while the server remains authoritative for the
56
+ same field. Not yet designed — revisit the first time an application needs
57
+ inline validation.
58
+
59
+ ## Release 1.1 candidates
60
+
61
+ Net-new scope, not gaps in 1.0. Release 1.1 will optimize for operational
62
+ concerns: maintaining high performance and focus on the essentials in
63
+ different deployment scenarios.
64
+
65
+ ### Lazy loading of page templates
66
+
67
+ Release 1.0 packages all pages into HTML `<template>` elements, delivered
68
+ along with the app shell as an HTML monolith on the first page `GET`.
69
+
70
+ For a low page count with fairly simple pages, the monolithic load is
71
+ probably faster than any other approach, and is definitely the simplest
72
+ approach.
73
+
74
+ For a high page count with complex pages, the one-time load of a monolith
75
+ could degrade performance on the first load, not to mention producing a
76
+ very cluttered result in View Page Source.
77
+
78
+ If we add addressable pages, the hub could do a non-blocking gradual load
79
+ of all templates. It could also load each template at first use.
80
+
81
+ ### Split data channel from static assets
82
+
83
+ Release 1.0 assumes that static assets are delivered through the same URL
84
+ as data responses.
85
+
86
+ But in Loadbare we have an advantage: the entire browser bundle is static,
87
+ all HTML, JavaScript and CSS is fixed at the time of release.
88
+
89
+ If the hub were configured with a URL for the data channel, the entire app
90
+ could be delivered from a static origin, such as an S3 bucket or static web
91
+ server.
92
+
93
+ ### Tree-shaking CSS by tag name
94
+
95
+ Release 1.0 concatenates every `.css` file under `src`, and every one inside
96
+ each imported package. A stylesheet is paired with nothing, so an app ships
97
+ the styles of every widget in every package it imports, used or not.
98
+
99
+ Naming a stylesheet for a tag — `<tag-name>.css` beside `<tag-name>.html`
100
+ and `<tag-name>.ts` — would let the builder ship only the stylesheets whose
101
+ tags survive expansion into the finished document. A `.css` file whose name
102
+ is not a tag would keep today's rule and always ship, which is what
103
+ `00-reset.css` and the rest of an app's own styling already are.
104
+
105
+ This would make a widget three files rather than two, so it changes the
106
+ file set in [Custom Elements](./reference/custom-elements.md) and the
107
+ "paired with nothing" rule in [CSS](./reference/css.md).
108
+
109
+ Most valuable against a large imported widget library, where an app uses a
110
+ small fraction of what ships. Revisit when a real app's `app.css` is big
111
+ enough to measure.
112
+
113
+ ### Data binding utilities
114
+
115
+ If a dev team wishes to make their own widgets that identify `lb-query`,
116
+ `lb-key`, `lb-cell`, they must repeat the code that is present in the hub.
117
+
118
+ Perhaps a utility that can be called, like `getDataScope(el)`, to help
119
+ clean up the code in these cases.
120
+
121
+ ### A language server
122
+
123
+ ...for Loadbare HTML.
124
+
125
+ ### I18N
126
+
127
+ Internationalization would require a potential extension to build-time
128
+ expansion allows a strings file. We could either preserve the fully
129
+ static build-time system and create multiple versions of `app.html`, or we
130
+ could add label hydration to the page navigation stage.
@@ -0,0 +1,228 @@
1
+ # Testing
2
+
3
+ A contributor's document, and a companion to [theory.md](./theory.md). It
4
+ describes how this package tests itself.
5
+
6
+ ## Position
7
+
8
+ Loadbare App is four layers with four different relationships to the browser,
9
+ and a single testing strategy would have to be the weakest of them. So there
10
+ are four tiers, and each is tested by the cheapest thing that constitutes
11
+ evidence for it.
12
+
13
+ The ordering principle is that a test should fail for the reason it is named
14
+ after. A test of expansion that needs a browser has bought a second failure
15
+ mode it does not want, and a test of navigation that runs under a DOM
16
+ emulation has given up the only failure mode it was looking for.
17
+
18
+ | Tier | Covers | Environment |
19
+ |------|--------------------------------------------------|-------------|
20
+ | 1 | Expansion and the build — `build/` | node |
21
+ | 2 | The engine — `server/`, and the Express adapter | node |
22
+ | 3 | Landing — `hub/lb-apply.ts`, `hub/lb-rows.ts` | jsdom |
23
+ | 4 | The hub — `hub/lb-hub.ts` | jsdom |
24
+
25
+ All four tiers run under `npm test` today and need no dependency that is not
26
+ already installed. A fifth environment — a real browser — is discussed at the
27
+ end and is not built.
28
+
29
+ ## The runner
30
+
31
+ `node:test`, run through `tsx`:
32
+
33
+ ```
34
+ npm test
35
+ ```
36
+
37
+ No test framework is installed, because none is needed. Node ships the
38
+ runner, `tsx` is already here, and jsdom is already here for expansion.
39
+
40
+ Tests live in `tests/`, one file per source file, named for it:
41
+ `tests/expand.test.ts` covers `build/expand.ts`.
42
+
43
+ Write a definition inline, beside the assertion that reads it — a two-line
44
+ definition is shorter than the reference that would point at it.
45
+ `tests/fixtures/` holds only what has to be a file: the checks that run while
46
+ definitions are being loaded, and the directory ordering that lets an
47
+ application override a built-in.
48
+
49
+ ## The console is an interface
50
+
51
+ The browser half of Loadbare App reports every failure it survives through
52
+ `console.error` and `console.warn` — a query with no scope, a list widget
53
+ with no template, a change event with no coordinates. These are not
54
+ diagnostics. They are the framework's entire error channel on the client, and
55
+ the behavior under test is frequently *that Loadbare reported and carried on*
56
+ rather than that it produced a value.
57
+
58
+ So tests assert on that channel. `tests/helpers/console.ts` is the seam that
59
+ makes it pleasant rather than fiddly.
60
+
61
+ ## Tier 1 — Expansion and the build
62
+
63
+ Expansion is string in, string out, with no data, no browser, and no clock.
64
+ It is also the layer whose failure mode is settled: it throws rather than
65
+ ships. That combination makes it both the easiest tier and the one carrying
66
+ the most of what Loadbare promises, so it is first.
67
+
68
+ **Rejections.** Every one of these is a build that must not produce output:
69
+
70
+ - an `lb-*` tag with no definition
71
+ - `exp-foo` supplied to a definition with no `{{foo}}`
72
+ - `{{camelCase}}` in a definition, which no tag could ever supply
73
+ - a cycle in the definition graph
74
+ - two `lb-template` destinations with one name
75
+ - two authored templates for one destination
76
+ - a template naming a destination the definition does not have
77
+ - content written inside a definition with no `lb-slot`
78
+ - more than one `lb-slot`
79
+
80
+ **Substitution.** An unset placeholder drops the attribute rather than
81
+ emitting it empty, which is what HTML's boolean attributes require and what
82
+ `readonly="{{readonly}}"` in `lb-input` depends on. Whitespace around a text
83
+ placeholder is preserved, because `{{label}} <input>` needs that space.
84
+ Placeholders inside `<template>` content are expanded, which is why
85
+ `lb-picker` can ship a row template of its own. `exp-` attributes survive on
86
+ the expanded tag, so what ships shows what was asked for beside what it
87
+ produced.
88
+
89
+ **Injection is impossible by construction**, because substitution goes
90
+ through `setAttribute` and node values rather than through a string. That
91
+ claim gets a test: a parameter whose value is markup ships as text. It exists
92
+ to fail if anyone ever reaches for string concatenation here.
93
+
94
+ **Golden files.** These live in `@loadbare/demo`, not here. Each demo
95
+ application is built by the installed `loadbare-app-build` and its assembled
96
+ `app.html` compared to a stored copy — the whole chain at once, including the
97
+ origin ordering that lets an application override a widget from a package,
98
+ exercised the way a consumer exercises it rather than by wiring this
99
+ package's own modules together. See
100
+ [`packages/demo/AGENTS.md`](../../demo/AGENTS.md).
101
+
102
+ Review the golden files rather than blessing them. `npm run test:golden -w
103
+ @loadbare/demo` rewrites them; the diff is the point, and a diff nobody read
104
+ is a test nobody ran.
105
+
106
+ The rest of the build — `assemble`, `elements`, `pages`, `styles`,
107
+ `package-css` — is tested the same way and in the same tier, since none of it
108
+ needs a browser either.
109
+
110
+ ## Tier 2 — The engine
111
+
112
+ `createHub` takes a plain object and returns an object. Nothing in
113
+ `server/lb-server.ts` opens a socket, so the fixtures are counting stubs.
114
+
115
+ - `beforeGet` runs before any query, and the whole query set runs after it
116
+ - an unknown page answers `{}` and says so
117
+ - an unknown query name in a refresh set is skipped, and its siblings run
118
+ - an action the page did not declare is refused — the rule that keeps the
119
+ wire from reaching anything the page has not published
120
+ - the refresh set runs after the action, against the same context
121
+ - **what the action stated wins over what the refresh produced**, because
122
+ that is how a `patch` reaches the browser at all, and it is one spread
123
+ operator away from silently reversing
124
+
125
+ `tests/lb-express.test.ts` runs against a listening app: the page comes from
126
+ the query string, an undeclared operation is refused with a 400, and a
127
+ malformed body does not throw.
128
+
129
+ ## Tier 3 — Landing
130
+
131
+ `applyData`, `applyTuple` and `applyRows` are the most intricate code in the
132
+ framework and the most likely to break in ways nobody notices. They are also
133
+ pure DOM: no fetch, no widget upgrade, no history. jsdom is real evidence
134
+ here.
135
+
136
+ - a native element receives its value as text, a hyphenated one as `lb-value`
137
+ - the root of a scope counts as a cell if it carries one, which is what makes
138
+ an `<option>` row possible
139
+ - a query with no scope is reported and skipped; several scopes for one query
140
+ are all filled; a projection landing on something that is not a list widget
141
+ is reported rather than thrown
142
+ - `rows` decides membership and order, so a key that did not arrive is gone
143
+ - `patch` disturbs only what it names, in contents and in position
144
+ - `data-rows` is counted from the DOM after reconciliation, so a set and a
145
+ patch ending in the same state report the same number
146
+ - the `place` callback is called for a fresh row always, and for an existing
147
+ row only under `rows`. That is today's behavior, not a decision — a patch
148
+ therefore never re-places a row whose sort key changed. The test states what
149
+ is true now, and is the one that flips if that changes.
150
+
151
+ **Two properties**, written as loops rather than with a library. Applying the
152
+ same `rows` twice is applying it once. And `rows(S)` reached through any
153
+ sequence of patches is `rows(S)` reached from empty. Convergence is the
154
+ actual contract of a reconciler, and those two say it better than twenty
155
+ examples.
156
+
157
+ ## Tier 4 — The hub
158
+
159
+ The widget half of this tier lives in `@loadbare/widgets` and is tested
160
+ there, against the same jsdom harness described below — see
161
+ [`packages/widgets/AGENTS.md`](../../widgets/AGENTS.md). What follows applies
162
+ to both, and the hub is what remains here.
163
+
164
+ Widgets are small and their logic is local, so jsdom carries them: a value
165
+ reaches the control the widget owns, a change dispatches the declared action,
166
+ an absent control or absent action is reported rather than thrown,
167
+ `lb-options` turns a key into a value and removes an emptied `<optgroup>`,
168
+ `lb-table` groups and sorts, removes an emptied section heading, and takes
169
+ its `colSpan` from the row template the page wrote.
170
+
171
+ A widget extends `HTMLElement` and registers itself as its module loads, so
172
+ unlike tier 3 it needs a window before the module exists. `installWindow()`
173
+ puts one in place and the widget module is imported after it, dynamically.
174
+ One window per file, which is what running each file in its own process gives
175
+ for free — registration is global and permanent, so sharing a process across
176
+ widget files would mean sharing a registry.
177
+
178
+ What that buys is real upgrades: the constructor, `connectedCallback`, and
179
+ `attributeChangedCallback` for attributes already present. That last one is
180
+ the mechanism by which hydration and refresh are one operation, and it
181
+ behaves the same in jsdom as in a browser. What it does not buy is layout,
182
+ painting, or navigation.
183
+
184
+ ## Not built: the browser tier
185
+
186
+ The hub is where jsdom stops being evidence. Fetch, `history.pushState`,
187
+ `popstate`, and the claim that insertion and hydration in one synchronous
188
+ block never paint an empty frame are all statements about a browser.
189
+
190
+ No browser test runner is installed and none of the following exists. They
191
+ are recorded here as the shape of the work, not as coverage:
192
+
193
+ 1. a cold load fills `<main>` with no empty flash
194
+ 2. `lb-nav-link` pushes state, swaps the host and lands data; `popstate`
195
+ reverses it; an ordinary anchor is not hijacked
196
+ 3. a native action button produces the same event a widget produces
197
+ 4. the wizard never displays a step the server has not confirmed
198
+ 5. the view-source invariant
199
+
200
+ The last is the one worth a browser. Loadbare's central claim is that no
201
+ markup exists in the DOM that is not in view-source, and that the difference
202
+ between the two is exactly the dynamic half. That is a property, it is stated
203
+ precisely in [theory.md](./theory.md), and it should be enforced mechanically
204
+ rather than believed: fetch the shell as text, compare the element set
205
+ against the live document after interaction, and assert that the only
206
+ additions are clones of row templates.
207
+
208
+ See [TODO.md](../TODO.md) for this and the rest of the open work.
209
+
210
+ ## Continuous integration
211
+
212
+ ```
213
+ npm run typecheck && npm run format:check && npm test
214
+ ```
215
+
216
+ Add the browser specs as a separate job if they are ever written, so the four
217
+ tiers here stay fast enough to run on every save.
218
+
219
+ ## One change the tests want
220
+
221
+ The client reports through `console.error` and `console.warn` directly, from
222
+ roughly twenty places across `hub/` and `@loadbare/widgets`. Routing those through a
223
+ single `report()` in `core/` would give the tests one seam to observe instead
224
+ of a global to mock per file, and would put the framework's error channel in
225
+ the same file as the rest of its vocabulary. It is a small change and it is
226
+ not urgent, but it is the only place where testing asks anything of the
227
+ design.
228
+ </content>