@loadbare/app 0.6.0 → 0.7.1

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 (122) hide show
  1. package/dist/build/assemble.js +1 -0
  2. package/dist/build/assemble.js.map +1 -0
  3. package/dist/build/cli.js +1 -0
  4. package/dist/build/cli.js.map +1 -0
  5. package/dist/build/elements.js +1 -0
  6. package/dist/build/elements.js.map +1 -0
  7. package/dist/build/expand.js +1 -0
  8. package/dist/build/expand.js.map +1 -0
  9. package/dist/build/format.js +1 -0
  10. package/dist/build/format.js.map +1 -0
  11. package/dist/build/locations.d.ts +2 -2
  12. package/dist/build/locations.d.ts.map +1 -1
  13. package/dist/build/locations.js +3 -4
  14. package/dist/build/locations.js.map +1 -0
  15. package/dist/build/origins.js +1 -0
  16. package/dist/build/origins.js.map +1 -0
  17. package/dist/build/package-root.js +1 -0
  18. package/dist/build/package-root.js.map +1 -0
  19. package/dist/build/pages.d.ts +4 -0
  20. package/dist/build/pages.d.ts.map +1 -1
  21. package/dist/build/pages.js +5 -0
  22. package/dist/build/pages.js.map +1 -0
  23. package/dist/build/styles.js +1 -0
  24. package/dist/build/styles.js.map +1 -0
  25. package/dist/core/lb-constants.js +1 -0
  26. package/dist/core/lb-constants.js.map +1 -0
  27. package/dist/core/lb-types.d.ts +1 -1
  28. package/dist/core/lb-types.d.ts.map +1 -1
  29. package/dist/core/lb-types.js +2 -1
  30. package/dist/core/lb-types.js.map +1 -0
  31. package/dist/hub/lb-apply.d.ts +11 -1
  32. package/dist/hub/lb-apply.d.ts.map +1 -1
  33. package/dist/hub/lb-apply.js +50 -13
  34. package/dist/hub/lb-apply.js.map +1 -0
  35. package/dist/hub/lb-hub.browser.d.ts +1 -1
  36. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  37. package/dist/hub/lb-hub.browser.js +143 -112
  38. package/dist/hub/lb-hub.browser.js.map +1 -0
  39. package/dist/server/lb-express.d.ts +1 -1
  40. package/dist/server/lb-express.d.ts.map +1 -1
  41. package/dist/server/lb-express.js +1 -0
  42. package/dist/server/lb-express.js.map +1 -0
  43. package/dist/server/lb-server.d.ts +1 -1
  44. package/dist/server/lb-server.d.ts.map +1 -1
  45. package/dist/server/lb-server.js +2 -1
  46. package/dist/server/lb-server.js.map +1 -0
  47. package/docs/TECHREF-1.0.md +158 -43
  48. package/docs/analysis-accidental-complexity.md +149 -0
  49. package/docs/reference/custom-elements.md +27 -17
  50. package/docs/reference/data-binding.md +88 -29
  51. package/docs/reference/server.md +18 -0
  52. package/docs/reference/widgets.md +3 -3
  53. package/docs/theory.md +114 -1
  54. package/docs/tutorials/072-inserting-into-a-list.md +14 -11
  55. package/docs/tutorials/080-widget-requests.md +9 -26
  56. package/package.json +2 -2
  57. package/dist/tests/assemble.test.d.ts +0 -8
  58. package/dist/tests/assemble.test.d.ts.map +0 -1
  59. package/dist/tests/assemble.test.js +0 -210
  60. package/dist/tests/elements.test.d.ts +0 -8
  61. package/dist/tests/elements.test.d.ts.map +0 -1
  62. package/dist/tests/elements.test.js +0 -118
  63. package/dist/tests/expand.test.d.ts +0 -10
  64. package/dist/tests/expand.test.d.ts.map +0 -1
  65. package/dist/tests/expand.test.js +0 -253
  66. package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
  67. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
  68. package/dist/tests/fixtures/elements/collision/imports.js +0 -1
  69. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
  70. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
  71. package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
  72. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
  73. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
  74. package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
  75. package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
  76. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
  77. package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
  78. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
  79. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
  80. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
  81. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
  82. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
  83. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
  84. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
  85. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
  86. package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
  87. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
  88. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
  89. package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
  90. package/dist/tests/helpers/console.d.ts +0 -20
  91. package/dist/tests/helpers/console.d.ts.map +0 -1
  92. package/dist/tests/helpers/console.js +0 -28
  93. package/dist/tests/helpers/dom.d.ts +0 -18
  94. package/dist/tests/helpers/dom.d.ts.map +0 -1
  95. package/dist/tests/helpers/dom.js +0 -22
  96. package/dist/tests/helpers/hub.d.ts +0 -73
  97. package/dist/tests/helpers/hub.d.ts.map +0 -1
  98. package/dist/tests/helpers/hub.js +0 -151
  99. package/dist/tests/lb-apply.test.d.ts +0 -8
  100. package/dist/tests/lb-apply.test.d.ts.map +0 -1
  101. package/dist/tests/lb-apply.test.js +0 -177
  102. package/dist/tests/lb-express.test.d.ts +0 -14
  103. package/dist/tests/lb-express.test.d.ts.map +0 -1
  104. package/dist/tests/lb-express.test.js +0 -243
  105. package/dist/tests/lb-hub.test.d.ts +0 -14
  106. package/dist/tests/lb-hub.test.d.ts.map +0 -1
  107. package/dist/tests/lb-hub.test.js +0 -319
  108. package/dist/tests/lb-list.test.d.ts +0 -12
  109. package/dist/tests/lb-list.test.d.ts.map +0 -1
  110. package/dist/tests/lb-list.test.js +0 -339
  111. package/dist/tests/lb-server.test.d.ts +0 -9
  112. package/dist/tests/lb-server.test.d.ts.map +0 -1
  113. package/dist/tests/lb-server.test.js +0 -546
  114. package/dist/tests/origins.test.d.ts +0 -10
  115. package/dist/tests/origins.test.d.ts.map +0 -1
  116. package/dist/tests/origins.test.js +0 -387
  117. package/dist/tests/pages.test.d.ts +0 -6
  118. package/dist/tests/pages.test.d.ts.map +0 -1
  119. package/dist/tests/pages.test.js +0 -148
  120. package/dist/tests/styles.test.d.ts +0 -7
  121. package/dist/tests/styles.test.d.ts.map +0 -1
  122. package/dist/tests/styles.test.js +0 -76
@@ -1 +0,0 @@
1
- {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest/imports.ts"],"names":[],"mappings":";AAAA,wBAAgC"}
@@ -1 +0,0 @@
1
- export default ["@acme/widget"];
@@ -1,3 +0,0 @@
1
- declare const _default: (string | number)[];
2
- export default _default;
3
- //# sourceMappingURL=imports.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-bad-entry/imports.ts"],"names":[],"mappings":";AAAA,wBAAoC"}
@@ -1 +0,0 @@
1
- export default ["@acme/widget", 42];
@@ -1,5 +0,0 @@
1
- declare const _default: {
2
- "acme-widget": string;
3
- };
4
- export default _default;
5
- //# sourceMappingURL=imports.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/manifest-not-array/imports.ts"],"names":[],"mappings":";;;AAAA,wBAAiD"}
@@ -1 +0,0 @@
1
- export default { "acme-widget": "@acme/widget" };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=acme-widget.browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"acme-widget.browser.d.ts","sourceRoot":"","sources":["../../../../../tests/fixtures/elements/pkg/acme-widget.browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC"}
@@ -1,6 +0,0 @@
1
- /**
2
- * A widget script that forgot to say `.browser` — the fixture behind the
3
- * error that names the file instead of reporting an absence.
4
- */
5
- export {};
6
- //# sourceMappingURL=app-box.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"app-box.d.ts","sourceRoot":"","sources":["../../../../../../tests/fixtures/elements/unmarked/widgets/app-box.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC"}
@@ -1,20 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,28 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * A document for tier 3 — see docs/testing.md.
3
- *
4
- * The landing code touches nothing global: it is handed a root and calls
5
- * `matches`, `querySelectorAll`, `cloneNode` and `setAttribute` on it. So a
6
- * test needs a parsed tree and nothing else — no globals installed, no
7
- * custom elements registered, no window on the loose between test files.
8
- *
9
- * Markup is parsed as the body of a document rather than into a detached
10
- * `<div>`, because the parser's content models are half of what this code
11
- * exists to work around: a `<tr>` assigned to a div's innerHTML is discarded,
12
- * and a test that lost its rows would be testing the parser.
13
- */
14
- /** 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
@@ -1 +0,0 @@
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"}
@@ -1,22 +0,0 @@
1
- /**
2
- * A document for tier 3 — see docs/testing.md.
3
- *
4
- * The landing code touches nothing global: it is handed a root and calls
5
- * `matches`, `querySelectorAll`, `cloneNode` and `setAttribute` on it. So a
6
- * test needs a parsed tree and nothing else — no globals installed, no
7
- * custom elements registered, no window on the loose between test files.
8
- *
9
- * Markup is parsed as the body of a document rather than into a detached
10
- * `<div>`, because the parser's content models are half of what this code
11
- * exists to work around: a `<tr>` assigned to a div's innerHTML is discarded,
12
- * and a test that lost its rows would be testing the parser.
13
- */
14
- import { JSDOM } from "jsdom";
15
- /** Parse markup and hand back the body holding it. */
16
- export function body(html) {
17
- return new JSDOM(`<!doctype html><body>${html}</body>`).window.document.body;
18
- }
19
- /** Parse markup and hand back its first element. */
20
- export function one(html) {
21
- return body(html).firstElementChild;
22
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * A window for 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. The hub is the opposite. It extends `HTMLElement`, it
6
- * registers itself with `customElements` when its module loads, and it
7
- * reaches `document`, `history` and `location` on its own. So the window has
8
- * to exist before the module does, which is why the hub is imported
9
- * dynamically, after `installWindow()` has run:
10
- *
11
- * installWindow();
12
- * await import("../hub/lb-hub.browser");
13
- *
14
- * One window per test file, since registration is global and permanent.
15
- *
16
- * Two things the hub reaches for do not exist in jsdom 25 and are supplied
17
- * here. `fetch` is absent, and a scripted one is what a test wants in any
18
- * case. `HTMLDialogElement.showModal` is absent, so it is recorded rather
19
- * than performed. Everything else — `history.pushState`, `popstate`, event
20
- * delegation, form controls — is jsdom's own.
21
- */
22
- import type { HubData, HubRequest } from "../../core/lb-types";
23
- /** One round trip the hub asked for. */
24
- export interface Sent {
25
- page: string;
26
- request: HubRequest | Record<string, never>;
27
- }
28
- export interface Server {
29
- /** Every round trip, in order. */
30
- sent: Sent[];
31
- /** What the next round trip answers with. Replace it per test. */
32
- answer: HubData;
33
- /** Set to fail the next round trip instead of answering it. */
34
- fail?: string;
35
- }
36
- /** The scripted server every test in a file shares. */
37
- export declare const server: Server;
38
- export declare function installWindow(): void;
39
- /**
40
- * Put a built document in place and hand back the hub in it.
41
- *
42
- * `pages` are the page hosts the builder would have written, keyed by stub —
43
- * see build/assemble.ts. `chrome` is what goes inside `<lb-hub>` beside
44
- * `<main>`. `path` is where the browser is when the hub connects, and it is
45
- * reset for every mount so no test inherits where the last one navigated to.
46
- *
47
- * Connecting the hub is what runs `connectedCallback`, and that navigates,
48
- * so the caller awaits the first round trip before asserting.
49
- */
50
- export declare function mountHub(chrome: string, pages?: Record<string, string>, path?: string): Promise<Element>;
51
- /**
52
- * Let the hub's pending round trip finish.
53
- *
54
- * `navigate` is async and nothing hands a test its promise, so a test waits
55
- * on the microtask queue the way the page does. Two turns: one for the fetch
56
- * and one for reading its body.
57
- */
58
- export declare function settle(): Promise<void>;
59
- /**
60
- * Step back and let the hub answer.
61
- *
62
- * `history.back()` is queued rather than synchronous, so a test waits for
63
- * the `popstate` the hub is listening for and then for the round trip it
64
- * starts.
65
- */
66
- export declare function back(): Promise<void>;
67
- /** Fire the event a person's click fires. */
68
- export declare function click(el: Element): Promise<void>;
69
- /** Fire the event a form's submit fires. */
70
- export declare function submit(form: Element): Promise<void>;
71
- /** Dispatch a request the way a widget does. */
72
- export declare function send(el: Element, detail: HubRequest | undefined): Promise<void>;
73
- //# sourceMappingURL=hub.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hub.d.ts","sourceRoot":"","sources":["../../../tests/helpers/hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAyB/D,wCAAwC;AACxC,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,MAAM;IACrB,kCAAkC;IAClC,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,uDAAuD;AACvD,eAAO,MAAM,MAAM,EAAE,MAAiC,CAAC;AAEvD,wBAAgB,aAAa,IAAI,IAAI,CAgDpC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAiB,EAC7C,IAAI,SAAM,GACT,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;;GAMG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAG5C;AAED;;;;;;GAMG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1C;AAED,6CAA6C;AAC7C,wBAAsB,KAAK,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKtD;AAED,4CAA4C;AAC5C,wBAAsB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzD;AAED,gDAAgD;AAChD,wBAAsB,IAAI,CACxB,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,UAAU,GAAG,SAAS,GAC7B,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -1,151 +0,0 @@
1
- /**
2
- * A window for 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. The hub is the opposite. It extends `HTMLElement`, it
6
- * registers itself with `customElements` when its module loads, and it
7
- * reaches `document`, `history` and `location` on its own. So the window has
8
- * to exist before the module does, which is why the hub is imported
9
- * dynamically, after `installWindow()` has run:
10
- *
11
- * installWindow();
12
- * await import("../hub/lb-hub.browser");
13
- *
14
- * One window per test file, since registration is global and permanent.
15
- *
16
- * Two things the hub reaches for do not exist in jsdom 25 and are supplied
17
- * here. `fetch` is absent, and a scripted one is what a test wants in any
18
- * case. `HTMLDialogElement.showModal` is absent, so it is recorded rather
19
- * than performed. Everything else — `history.pushState`, `popstate`, event
20
- * delegation, form controls — is jsdom's own.
21
- */
22
- import { JSDOM, VirtualConsole } from "jsdom";
23
- /** The globals the hub reaches for as it loads and runs. */
24
- const GLOBALS = [
25
- "window",
26
- "document",
27
- "customElements",
28
- "HTMLElement",
29
- "HTMLFormElement",
30
- "HTMLInputElement",
31
- "HTMLSelectElement",
32
- "HTMLTextAreaElement",
33
- "HTMLTemplateElement",
34
- "HTMLDialogElement",
35
- "HTMLAnchorElement",
36
- "Element",
37
- "Node",
38
- "Event",
39
- "CustomEvent",
40
- "MouseEvent",
41
- "DocumentFragment",
42
- "history",
43
- "location",
44
- ];
45
- /** The scripted server every test in a file shares. */
46
- export const server = { sent: [], answer: {} };
47
- export function installWindow() {
48
- // jsdom reports a real anchor's navigation as an unimplemented feature.
49
- // That is the point of the test that clicks one — the hub left it to the
50
- // browser — so the complaint is dropped rather than printed.
51
- const quiet = new VirtualConsole();
52
- quiet.sendTo(console, { omitJSDOMErrors: true });
53
- const { window } = new JSDOM(`<!doctype html><body></body>`, {
54
- url: "http://localhost/",
55
- virtualConsole: quiet,
56
- });
57
- for (const name of GLOBALS) {
58
- globalThis[name] = window[name];
59
- }
60
- // The hub registers `popstate` on the global rather than on a window it
61
- // was handed, so the global's listener registry has to be the window's.
62
- globalThis.addEventListener = window.addEventListener.bind(window);
63
- globalThis.removeEventListener = window.removeEventListener.bind(window);
64
- // Recorded, not performed: jsdom has <dialog> but none of its behavior.
65
- // The hub only ever asks whether it is open and opens it, so that pair is
66
- // the whole of what a test needs.
67
- const dialog = window.HTMLDialogElement.prototype;
68
- dialog.showModal = function () {
69
- this.setAttribute("open", "");
70
- };
71
- dialog.close = function () {
72
- this.removeAttribute("open");
73
- };
74
- server.sent = [];
75
- server.answer = {};
76
- delete server.fail;
77
- globalThis.fetch = (async (url, init) => {
78
- server.sent.push({
79
- page: new URL(url, "http://localhost/").searchParams.get("page") ?? "",
80
- request: JSON.parse(init.body),
81
- });
82
- if (server.fail)
83
- throw new Error(server.fail);
84
- return {
85
- ok: true,
86
- json: async () => server.answer,
87
- };
88
- });
89
- }
90
- /**
91
- * Put a built document in place and hand back the hub in it.
92
- *
93
- * `pages` are the page hosts the builder would have written, keyed by stub —
94
- * see build/assemble.ts. `chrome` is what goes inside `<lb-hub>` beside
95
- * `<main>`. `path` is where the browser is when the hub connects, and it is
96
- * reset for every mount so no test inherits where the last one navigated to.
97
- *
98
- * Connecting the hub is what runs `connectedCallback`, and that navigates,
99
- * so the caller awaits the first round trip before asserting.
100
- */
101
- export async function mountHub(chrome, pages = { index: "" }, path = "/") {
102
- history.replaceState(null, "", path);
103
- const hosts = Object.entries(pages)
104
- .map(([name, html]) => `<template lb-page="${name}">${html}</template>`)
105
- .join("");
106
- document.body.innerHTML = `<lb-hub><main></main>${chrome}</lb-hub>${hosts}`;
107
- await settle();
108
- return document.body.firstElementChild;
109
- }
110
- /**
111
- * Let the hub's pending round trip finish.
112
- *
113
- * `navigate` is async and nothing hands a test its promise, so a test waits
114
- * on the microtask queue the way the page does. Two turns: one for the fetch
115
- * and one for reading its body.
116
- */
117
- export async function settle() {
118
- for (let i = 0; i < 4; i++)
119
- await Promise.resolve();
120
- await new Promise((r) => setTimeout(r, 0));
121
- }
122
- /**
123
- * Step back and let the hub answer.
124
- *
125
- * `history.back()` is queued rather than synchronous, so a test waits for
126
- * the `popstate` the hub is listening for and then for the round trip it
127
- * starts.
128
- */
129
- export async function back() {
130
- const stepped = new Promise((resolve) => {
131
- globalThis.addEventListener("popstate", () => resolve(), { once: true });
132
- });
133
- history.back();
134
- await stepped;
135
- await settle();
136
- }
137
- /** Fire the event a person's click fires. */
138
- export async function click(el) {
139
- el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
140
- await settle();
141
- }
142
- /** Fire the event a form's submit fires. */
143
- export async function submit(form) {
144
- form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
145
- await settle();
146
- }
147
- /** Dispatch a request the way a widget does. */
148
- export async function send(el, detail) {
149
- el.dispatchEvent(new CustomEvent("lb-request", { bubbles: true, detail }));
150
- await settle();
151
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Landing data on a live host — see docs/testing.md, tier 3.
3
- *
4
- * hub/lb-apply.ts is pure DOM: no fetch, no widget upgrade, no history.
5
- * jsdom is real evidence for it, which is not true one file further on.
6
- */
7
- export {};
8
- //# sourceMappingURL=lb-apply.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-apply.test.d.ts","sourceRoot":"","sources":["../../tests/lb-apply.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,177 +0,0 @@
1
- /**
2
- * Landing data on a live host — see docs/testing.md, tier 3.
3
- *
4
- * hub/lb-apply.ts is pure DOM: no fetch, no widget upgrade, no history.
5
- * jsdom is real evidence for it, which is not true one file further on.
6
- */
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import { applyData, applyRow } from "../hub/lb-apply";
10
- import { body, one } from "./helpers/dom";
11
- import { captured } from "./helpers/console";
12
- // --- One row into one scope ---------------------------------------------
13
- describe("applyRow", () => {
14
- it("gives a native element its value as text", () => {
15
- const scope = one(`<div><span lb-cell="name"></span></div>`);
16
- applyRow(scope, { name: "Ada" });
17
- assert.equal(scope.querySelector("span").textContent, "Ada");
18
- });
19
- it("gives a widget its value as an attribute", () => {
20
- // A custom element owns whatever control it wraps, so it receives the
21
- // value and renders it itself. The test is lexical — a hyphen in the tag
22
- // name — and the hub holds no knowledge of any particular element.
23
- const scope = one(`<div><lb-input lb-cell="name"></lb-input></div>`);
24
- applyRow(scope, { name: "Ada" });
25
- const widget = scope.querySelector("lb-input");
26
- assert.equal(widget.getAttribute("lb-value"), "Ada");
27
- assert.equal(widget.textContent, "", "the widget's children are its own");
28
- });
29
- it("counts the root as a cell when it carries one", () => {
30
- // <option>'s content model is text, so an option row has to be the cell
31
- // it displays. Requiring a wrapper would require an element HTML does
32
- // not allow.
33
- const scope = one(`<option lb-cell="name"></option>`);
34
- applyRow(scope, { name: "Ada" });
35
- assert.equal(scope.textContent, "Ada");
36
- });
37
- it("fills every element naming the same cell", () => {
38
- const scope = one(`<div><b lb-cell="name"></b><i lb-cell="name"></i></div>`);
39
- applyRow(scope, { name: "Ada" });
40
- assert.equal(scope.querySelector("b").textContent, "Ada");
41
- assert.equal(scope.querySelector("i").textContent, "Ada");
42
- });
43
- it("empties a native root that is a cell, children and all", () => {
44
- // A root that is a cell is a leaf, and a native one lands its value as
45
- // text — so the children go, because setting textContent is what
46
- // replacing an element's content means.
47
- //
48
- // The case this is for cannot hit it: <option> is a leaf by content
49
- // model. But nothing stops an author writing a cell on a <tr> that has
50
- // cells inside it, and this is what they would get.
51
- const scope = body(`<table><tbody><tr lb-cell="line"><td lb-cell="amount"></td></tr></tbody></table>`).querySelector("tr");
52
- applyRow(scope, { line: "Total", amount: "10" });
53
- assert.equal(scope.textContent, "Total");
54
- assert.equal(scope.children.length, 0, "the amount cell is gone");
55
- });
56
- it("keeps the children of a widget root that is a cell", () => {
57
- // The same shape with a hyphen in the tag name is unharmed, because a
58
- // widget receives its value as an attribute and renders it itself. The
59
- // asymmetry follows from what a cell means on each kind of element.
60
- const scope = one(`<lb-input lb-cell="line"><input /></lb-input>`);
61
- applyRow(scope, { line: "Total" });
62
- assert.equal(scope.getAttribute("lb-value"), "Total");
63
- assert.equal(scope.children.length, 1);
64
- });
65
- it("leaves a cell the row did not name alone", () => {
66
- const scope = one(`<div><span lb-cell="name">Ada</span><span lb-cell="role">lead</span></div>`);
67
- applyRow(scope, { role: "staff" });
68
- assert.equal(scope.querySelector(`[lb-cell="name"]`).textContent, "Ada");
69
- assert.equal(scope.querySelector(`[lb-cell="role"]`).textContent, "staff");
70
- });
71
- it("says nothing about a cell with nowhere to land", () => {
72
- const scope = one(`<div><span lb-cell="name"></span></div>`);
73
- assert.doesNotThrow(() => applyRow(scope, { absent: "x" }));
74
- });
75
- it("lands a value as text, so data cannot become markup", () => {
76
- // The same guarantee expansion makes at build time, made here at run
77
- // time and by the same means: the DOM API rather than a string.
78
- const scope = one(`<div><span lb-cell="name"></span></div>`);
79
- applyRow(scope, { name: `<img src=x onerror=alert(1)>` });
80
- assert.equal(scope.querySelectorAll("img").length, 0);
81
- assert.equal(scope.querySelector("span").textContent, `<img src=x onerror=alert(1)>`);
82
- });
83
- it("lands the empty string as the empty string", () => {
84
- const scope = one(`<div><span lb-cell="name">Ada</span></div>`);
85
- applyRow(scope, { name: "" });
86
- assert.equal(scope.querySelector("span").textContent, "");
87
- });
88
- });
89
- // --- A whole response ----------------------------------------------------
90
- describe("applyData", () => {
91
- it("lands each result in the scope its name binds", () => {
92
- const root = body(`<div lb-row="greeting"><span lb-cell="text"></span></div>` +
93
- `<div lb-row="count"><span lb-cell="n"></span></div>`);
94
- applyData(root, { greeting: { text: "hello" }, count: { n: "3" } });
95
- assert.equal(root.querySelector(`[lb-row="greeting"] span`).textContent, "hello");
96
- assert.equal(root.querySelector(`[lb-row="count"] span`).textContent, "3");
97
- });
98
- it("fills every scope naming the same name", () => {
99
- // One name shown in two places: the hub resolves by name and does not
100
- // care how many places asked.
101
- const root = body(`<div lb-row="total"><span lb-cell="n"></span></div>` +
102
- `<p lb-row="total"><span lb-cell="n"></span></p>`);
103
- applyData(root, { total: { n: "7" } });
104
- for (const el of root.querySelectorAll("span")) {
105
- assert.equal(el.textContent, "7");
106
- }
107
- });
108
- it("reports a name with no scope, and lands the rest anyway", async () => {
109
- const root = body(`<div lb-row="count"><span lb-cell="n"></span></div>`);
110
- const { warn } = await captured(() => applyData(root, { ghost: { x: "1" }, count: { n: "3" } }));
111
- assert.match(warn.join("\n"), /no scope for 'ghost', skipping/);
112
- assert.equal(root.querySelector("span").textContent, "3");
113
- });
114
- it("reconciles a list scope, with no widget in it at all", () => {
115
- // The repeater widget is gone: a plain element carrying lb-list and a
116
- // row template inside it is the whole of a list.
117
- const root = body(`<ul lb-list="members">` +
118
- `<template lb-key="id"><li lb-cell="name"></li></template>` +
119
- `</ul>`);
120
- applyData(root, {
121
- members: [
122
- { id: "1", name: "Ada" },
123
- { id: "2", name: "Bab" },
124
- ],
125
- });
126
- assert.deepEqual([...root.querySelectorAll("li")].map((el) => el.textContent), ["Ada", "Bab"]);
127
- });
128
- it("lands a patch in a list scope, because the name declared the shape", () => {
129
- // A patch and a row are both objects. What tells them apart is the
130
- // attribute that asked, not the value that arrived.
131
- const root = body(`<ul lb-list="members">` +
132
- `<template lb-key="id"><li lb-cell="name"></li></template>` +
133
- `</ul>`);
134
- applyData(root, { members: [{ id: "1", name: "Ada" }] });
135
- applyData(root, { members: { rows: [{ id: "2", name: "Bab" }] } });
136
- assert.deepEqual([...root.querySelectorAll("li")].map((el) => el.textContent), ["Ada", "Bab"]);
137
- });
138
- it("refuses a name bound as a list in one place and a row in another", async () => {
139
- // Cardinality is a property of the name, so this is a mistake in the
140
- // markup rather than a case to reconcile.
141
- const root = body(`<ul lb-list="members">` +
142
- `<template lb-key="id"><li lb-cell="name"></li></template>` +
143
- `</ul>` +
144
- `<div lb-row="members"><span lb-cell="name"></span></div>`);
145
- const { error } = await captured(() => applyData(root, { members: [{ id: "1", name: "Ada" }] }));
146
- assert.match(error.join("\n"), /a list in one place and a row in another/);
147
- assert.equal(root.querySelectorAll("li").length, 0, "nothing landed");
148
- });
149
- it("refuses rows arriving at a row scope", async () => {
150
- const root = body(`<div lb-row="members"><span lb-cell="n"></span></div>`);
151
- const { error } = await captured(() => applyData(root, { members: [{ id: "1" }] }));
152
- assert.match(error.join("\n"), /bound with lb-row but answered with rows/);
153
- });
154
- it("does not carry a row into a nested scope", () => {
155
- // A nested scope of either kind begins a new one, so a cell named alike
156
- // inside it belongs to the inner name.
157
- const root = body(`<div lb-row="outer"><span lb-cell="name"></span>` +
158
- `<p lb-row="inner"><span lb-cell="name"></span></p></div>`);
159
- applyData(root, { inner: { name: "in" }, outer: { name: "out" } });
160
- const [first, second] = root.querySelectorAll("span");
161
- assert.equal(first.textContent, "out");
162
- assert.equal(second.textContent, "in");
163
- });
164
- it("lands a column called op like any other", () => {
165
- // It used to be reserved, because the hub read it to tell a row from a
166
- // set. Cardinality is declared now, so nothing inspects a value and no
167
- // column name is spoken for.
168
- const root = body(`<div lb-row="thing"><span lb-cell="op"></span></div>`);
169
- applyData(root, { thing: { op: "rows" } });
170
- assert.equal(root.querySelector("span").textContent, "rows");
171
- });
172
- it("does nothing with an empty response", () => {
173
- const root = body(`<div lb-row="count"><span lb-cell="n">3</span></div>`);
174
- applyData(root, {});
175
- assert.equal(root.querySelector("span").textContent, "3");
176
- });
177
- });
@@ -1,14 +0,0 @@
1
- /**
2
- * The Express adapter — see docs/testing.md, tier 2.
3
- *
4
- * server/lb-express.ts is the only file that knows both HTTP and Hub. What
5
- * it owes is small and exact: one endpoint and no second, the page from the
6
- * query string, and an operation it does not yet handle refused rather than
7
- * half-handled.
8
- *
9
- * It is tested against a listening server rather than a mocked request,
10
- * because the things it can get wrong — a body that never parsed, a route
11
- * that swallows another — are things a mock would agree with.
12
- */
13
- export {};
14
- //# sourceMappingURL=lb-express.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lb-express.test.d.ts","sourceRoot":"","sources":["../../tests/lb-express.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}