@loadbare/app 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/assemble.js +1 -0
- package/dist/build/assemble.js.map +1 -0
- package/dist/build/cli.js +1 -0
- package/dist/build/cli.js.map +1 -0
- package/dist/build/elements.js +1 -0
- package/dist/build/elements.js.map +1 -0
- package/dist/build/expand.js +1 -0
- package/dist/build/expand.js.map +1 -0
- package/dist/build/format.js +1 -0
- package/dist/build/format.js.map +1 -0
- package/dist/build/locations.d.ts +2 -2
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +3 -4
- package/dist/build/locations.js.map +1 -0
- package/dist/build/origins.js +1 -0
- package/dist/build/origins.js.map +1 -0
- package/dist/build/package-root.js +1 -0
- package/dist/build/package-root.js.map +1 -0
- package/dist/build/pages.d.ts +4 -0
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +5 -0
- package/dist/build/pages.js.map +1 -0
- package/dist/build/styles.js +1 -0
- package/dist/build/styles.js.map +1 -0
- package/dist/core/lb-constants.js +1 -0
- package/dist/core/lb-constants.js.map +1 -0
- package/dist/core/lb-types.d.ts +1 -1
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +2 -1
- package/dist/core/lb-types.js.map +1 -0
- package/dist/hub/lb-apply.d.ts +11 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +50 -13
- package/dist/hub/lb-apply.js.map +1 -0
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +117 -109
- package/dist/hub/lb-hub.browser.js.map +1 -0
- package/dist/server/lb-express.d.ts +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +1 -0
- package/dist/server/lb-express.js.map +1 -0
- package/dist/server/lb-server.d.ts +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +2 -1
- package/dist/server/lb-server.js.map +1 -0
- package/docs/TECHREF-1.0.md +149 -42
- package/docs/analysis-accidental-complexity.md +149 -0
- package/docs/reference/custom-elements.md +10 -17
- package/docs/reference/data-binding.md +56 -27
- package/docs/reference/server.md +18 -0
- package/docs/reference/widgets.md +3 -3
- package/docs/theory.md +114 -1
- package/docs/tutorials/072-inserting-into-a-list.md +14 -11
- package/docs/tutorials/080-widget-requests.md +9 -26
- package/package.json +2 -2
- package/dist/tests/assemble.test.d.ts +0 -8
- package/dist/tests/assemble.test.d.ts.map +0 -1
- package/dist/tests/assemble.test.js +0 -210
- package/dist/tests/elements.test.d.ts +0 -8
- package/dist/tests/elements.test.d.ts.map +0 -1
- package/dist/tests/elements.test.js +0 -118
- package/dist/tests/expand.test.d.ts +0 -10
- package/dist/tests/expand.test.d.ts.map +0 -1
- package/dist/tests/expand.test.js +0 -253
- package/dist/tests/fixtures/elements/collision/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/imports.js +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/local/widgets/app-box.browser.js +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts +0 -2
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/pkg/acme-widget.browser.js +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts +0 -6
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/unmarked/widgets/app-box.js +0 -1
- package/dist/tests/helpers/console.d.ts +0 -20
- package/dist/tests/helpers/console.d.ts.map +0 -1
- package/dist/tests/helpers/console.js +0 -28
- package/dist/tests/helpers/dom.d.ts +0 -18
- package/dist/tests/helpers/dom.d.ts.map +0 -1
- package/dist/tests/helpers/dom.js +0 -22
- package/dist/tests/helpers/hub.d.ts +0 -73
- package/dist/tests/helpers/hub.d.ts.map +0 -1
- package/dist/tests/helpers/hub.js +0 -151
- package/dist/tests/lb-apply.test.d.ts +0 -8
- package/dist/tests/lb-apply.test.d.ts.map +0 -1
- package/dist/tests/lb-apply.test.js +0 -177
- package/dist/tests/lb-express.test.d.ts +0 -14
- package/dist/tests/lb-express.test.d.ts.map +0 -1
- package/dist/tests/lb-express.test.js +0 -243
- package/dist/tests/lb-hub.test.d.ts +0 -14
- package/dist/tests/lb-hub.test.d.ts.map +0 -1
- package/dist/tests/lb-hub.test.js +0 -319
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -339
- package/dist/tests/lb-server.test.d.ts +0 -9
- package/dist/tests/lb-server.test.d.ts.map +0 -1
- package/dist/tests/lb-server.test.js +0 -546
- package/dist/tests/origins.test.d.ts +0 -10
- package/dist/tests/origins.test.d.ts.map +0 -1
- package/dist/tests/origins.test.js +0 -387
- package/dist/tests/pages.test.d.ts +0 -6
- package/dist/tests/pages.test.d.ts.map +0 -1
- package/dist/tests/pages.test.js +0 -148
- package/dist/tests/styles.test.d.ts +0 -7
- package/dist/tests/styles.test.d.ts.map +0 -1
- package/dist/tests/styles.test.js +0 -76
|
@@ -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"}
|
|
@@ -1,243 +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
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { after, before, describe, it } from "node:test";
|
|
15
|
-
import express from "express";
|
|
16
|
-
import { LB_ENDPOINT } from "../core/lb-constants";
|
|
17
|
-
import { hubRoutes } from "../server/lb-express";
|
|
18
|
-
import { captured } from "./helpers/console";
|
|
19
|
-
const calls = [];
|
|
20
|
-
let contexts = 0;
|
|
21
|
-
const engine = {
|
|
22
|
-
async dataForPage(page, ctx) {
|
|
23
|
-
calls.push({ kind: "data", page, ctx });
|
|
24
|
-
return { greeting: { text: "hello" } };
|
|
25
|
-
},
|
|
26
|
-
async runAction(page, name, where, ctx) {
|
|
27
|
-
calls.push({ kind: "action", page, name, where, ctx });
|
|
28
|
-
if (name === "explode")
|
|
29
|
-
throw new Error("application code failed");
|
|
30
|
-
return { count: { n: "2" } };
|
|
31
|
-
},
|
|
32
|
-
async runCellChange(page, where, ctx) {
|
|
33
|
-
calls.push({ kind: "cell-change", page, where, ctx });
|
|
34
|
-
return { members: { n: "3" } };
|
|
35
|
-
},
|
|
36
|
-
async runRowDelete(page, where, ctx) {
|
|
37
|
-
calls.push({ kind: "tuple-delete", page, where, ctx });
|
|
38
|
-
return { members: { n: "4" } };
|
|
39
|
-
},
|
|
40
|
-
async runRowInsert(page, where, ctx) {
|
|
41
|
-
calls.push({ kind: "tuple-insert", page, where, ctx });
|
|
42
|
-
return { members: { n: "5" } };
|
|
43
|
-
},
|
|
44
|
-
async runRowUpdate(page, where, ctx) {
|
|
45
|
-
calls.push({ kind: "tuple-update", page, where, ctx });
|
|
46
|
-
return { members: { n: "6" } };
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
let base;
|
|
50
|
-
let server;
|
|
51
|
-
before(async () => {
|
|
52
|
-
const app = express();
|
|
53
|
-
app.use(hubRoutes(engine, () => {
|
|
54
|
-
contexts += 1;
|
|
55
|
-
return {};
|
|
56
|
-
}));
|
|
57
|
-
await new Promise((done) => {
|
|
58
|
-
server = app.listen(0, "127.0.0.1", () => done());
|
|
59
|
-
});
|
|
60
|
-
base = `http://127.0.0.1:${server.address().port}`;
|
|
61
|
-
});
|
|
62
|
-
after(() => void server.close());
|
|
63
|
-
function post(page, body, raw) {
|
|
64
|
-
return fetch(`${base}${LB_ENDPOINT}?page=${page}`, {
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: { "content-type": "application/json" },
|
|
67
|
-
body: raw ?? JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
describe("a request with no action", () => {
|
|
71
|
-
it("runs the page named in the query string", async () => {
|
|
72
|
-
calls.length = 0;
|
|
73
|
-
const res = await post("hello", {});
|
|
74
|
-
assert.equal(res.status, 200);
|
|
75
|
-
assert.deepEqual(await res.json(), { greeting: { text: "hello" } });
|
|
76
|
-
assert.equal(calls.length, 1);
|
|
77
|
-
assert.equal(calls[0].kind, "data");
|
|
78
|
-
assert.equal(calls[0].page, "hello");
|
|
79
|
-
});
|
|
80
|
-
it("names the empty page when the query string does not", async () => {
|
|
81
|
-
// The engine answers for a page it does not have; the route's job is only
|
|
82
|
-
// to pass on what it was given rather than to invent a default.
|
|
83
|
-
calls.length = 0;
|
|
84
|
-
await fetch(`${base}${LB_ENDPOINT}`, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: { "content-type": "application/json" },
|
|
87
|
-
body: "{}",
|
|
88
|
-
});
|
|
89
|
-
assert.equal(calls[0].page, "");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe("a request carrying an action", () => {
|
|
93
|
-
it("runs the action, with where the interaction happened", async () => {
|
|
94
|
-
calls.length = 0;
|
|
95
|
-
const res = await post("roster", {
|
|
96
|
-
action: "pick",
|
|
97
|
-
list: "members",
|
|
98
|
-
key: "7",
|
|
99
|
-
cell: "role",
|
|
100
|
-
value: "lead",
|
|
101
|
-
});
|
|
102
|
-
assert.equal(res.status, 200);
|
|
103
|
-
assert.deepEqual(await res.json(), { count: { n: "2" } });
|
|
104
|
-
assert.deepEqual(calls[0].where, {
|
|
105
|
-
list: "members",
|
|
106
|
-
row: undefined,
|
|
107
|
-
key: "7",
|
|
108
|
-
cell: "role",
|
|
109
|
-
value: "lead",
|
|
110
|
-
});
|
|
111
|
-
assert.equal(calls[0].name, "pick");
|
|
112
|
-
assert.equal(calls[0].page, "roster");
|
|
113
|
-
});
|
|
114
|
-
it("passes on an action carrying no binding at all", async () => {
|
|
115
|
-
// A button carries no value and may sit outside any scope. Undefined
|
|
116
|
-
// stays undefined rather than becoming the empty string.
|
|
117
|
-
calls.length = 0;
|
|
118
|
-
await post("counter", { action: "bump" });
|
|
119
|
-
assert.deepEqual(calls[0].where, {
|
|
120
|
-
list: undefined,
|
|
121
|
-
row: undefined,
|
|
122
|
-
key: undefined,
|
|
123
|
-
cell: undefined,
|
|
124
|
-
value: undefined,
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
it("refuses a reserved name it does not implement, rather than guessing", async () => {
|
|
128
|
-
calls.length = 0;
|
|
129
|
-
const { result: res, warn } = await captured(() => post("roster", { action: "lb-row-upsert", list: "members", key: "1" }));
|
|
130
|
-
assert.equal(res.status, 400);
|
|
131
|
-
assert.deepEqual(await res.json(), {});
|
|
132
|
-
assert.deepEqual(calls, [], "the engine was never reached");
|
|
133
|
-
assert.match(warn.join("\n"), /operation 'lb-row-upsert' not implemented/);
|
|
134
|
-
});
|
|
135
|
-
it("runs a cell-change, with where the interaction happened", async () => {
|
|
136
|
-
calls.length = 0;
|
|
137
|
-
const res = await post("roster", {
|
|
138
|
-
action: "lb-cell-change",
|
|
139
|
-
list: "members",
|
|
140
|
-
key: "7",
|
|
141
|
-
cell: "name",
|
|
142
|
-
value: "Ada",
|
|
143
|
-
});
|
|
144
|
-
assert.equal(res.status, 200);
|
|
145
|
-
assert.deepEqual(await res.json(), { members: { n: "3" } });
|
|
146
|
-
assert.deepEqual(calls[0].where, {
|
|
147
|
-
list: "members",
|
|
148
|
-
key: "7",
|
|
149
|
-
cell: "name",
|
|
150
|
-
value: "Ada",
|
|
151
|
-
});
|
|
152
|
-
assert.equal(calls[0].kind, "cell-change");
|
|
153
|
-
assert.equal(calls[0].page, "roster");
|
|
154
|
-
});
|
|
155
|
-
it("runs a tuple-delete, with the key of the row", async () => {
|
|
156
|
-
calls.length = 0;
|
|
157
|
-
const res = await post("roster", {
|
|
158
|
-
action: "lb-row-delete",
|
|
159
|
-
list: "members",
|
|
160
|
-
key: "7",
|
|
161
|
-
});
|
|
162
|
-
assert.equal(res.status, 200);
|
|
163
|
-
assert.deepEqual(await res.json(), { members: { n: "4" } });
|
|
164
|
-
assert.deepEqual(calls[0].where, { list: "members", key: "7" });
|
|
165
|
-
assert.equal(calls[0].kind, "tuple-delete");
|
|
166
|
-
assert.equal(calls[0].page, "roster");
|
|
167
|
-
});
|
|
168
|
-
it("runs a tuple-insert, with the gathered values", async () => {
|
|
169
|
-
calls.length = 0;
|
|
170
|
-
const res = await post("roster", {
|
|
171
|
-
action: "lb-row-insert",
|
|
172
|
-
list: "members",
|
|
173
|
-
values: { name: "Ada", role: "Analyst" },
|
|
174
|
-
});
|
|
175
|
-
assert.equal(res.status, 200);
|
|
176
|
-
assert.deepEqual(await res.json(), { members: { n: "5" } });
|
|
177
|
-
assert.deepEqual(calls[0].where, {
|
|
178
|
-
list: "members",
|
|
179
|
-
values: { name: "Ada", role: "Analyst" },
|
|
180
|
-
});
|
|
181
|
-
assert.equal(calls[0].kind, "tuple-insert");
|
|
182
|
-
assert.equal(calls[0].page, "roster");
|
|
183
|
-
});
|
|
184
|
-
it("runs a tuple-update, with the key and the gathered values", async () => {
|
|
185
|
-
calls.length = 0;
|
|
186
|
-
const res = await post("roster", {
|
|
187
|
-
action: "lb-row-update",
|
|
188
|
-
list: "members",
|
|
189
|
-
key: "7",
|
|
190
|
-
values: { name: "Ada", role: "Analyst" },
|
|
191
|
-
});
|
|
192
|
-
assert.equal(res.status, 200);
|
|
193
|
-
assert.deepEqual(await res.json(), { members: { n: "6" } });
|
|
194
|
-
assert.deepEqual(calls[0].where, {
|
|
195
|
-
list: "members",
|
|
196
|
-
key: "7",
|
|
197
|
-
values: { name: "Ada", role: "Analyst" },
|
|
198
|
-
});
|
|
199
|
-
assert.equal(calls[0].kind, "tuple-update");
|
|
200
|
-
assert.equal(calls[0].page, "roster");
|
|
201
|
-
});
|
|
202
|
-
it("refuses a body with no op", async () => {
|
|
203
|
-
calls.length = 0;
|
|
204
|
-
const { result: res } = await captured(() => post("roster", { name: "pick" }));
|
|
205
|
-
assert.equal(res.status, 400);
|
|
206
|
-
assert.deepEqual(calls, []);
|
|
207
|
-
});
|
|
208
|
-
it("survives an operation that throws, rather than crashing", async () => {
|
|
209
|
-
calls.length = 0;
|
|
210
|
-
const { result: res, error } = await captured(() => post("roster", { action: "explode", list: "members" }));
|
|
211
|
-
assert.equal(res.status, 500);
|
|
212
|
-
assert.deepEqual(await res.json(), {});
|
|
213
|
-
assert.match(error.join("\n"), /'explode' threw/);
|
|
214
|
-
// And the process is still serving.
|
|
215
|
-
const next = await post("hello", {});
|
|
216
|
-
assert.equal(next.status, 200);
|
|
217
|
-
});
|
|
218
|
-
it("survives a body that is not JSON", async () => {
|
|
219
|
-
calls.length = 0;
|
|
220
|
-
const res = await post("roster", null, "{ not json");
|
|
221
|
-
assert.ok(res.status >= 400, `expected a refusal, got ${res.status}`);
|
|
222
|
-
assert.deepEqual(calls, []);
|
|
223
|
-
// And the process is still serving. A parse failure is one request's
|
|
224
|
-
// problem, not the server's.
|
|
225
|
-
const next = await post("hello", {});
|
|
226
|
-
assert.equal(next.status, 200);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("the context", () => {
|
|
230
|
-
it("is built once per request, by the application", async () => {
|
|
231
|
-
// Hub's only requirement is that the context is built per request,
|
|
232
|
-
// after whatever establishes identity. Holding one for the life of the
|
|
233
|
-
// process is the mistake this asserts against.
|
|
234
|
-
contexts = 0;
|
|
235
|
-
calls.length = 0;
|
|
236
|
-
await post("hello", {});
|
|
237
|
-
await post("hello", {});
|
|
238
|
-
await post("counter", { action: "bump" });
|
|
239
|
-
assert.equal(contexts, 3);
|
|
240
|
-
assert.notEqual(calls[0].ctx, calls[1].ctx);
|
|
241
|
-
assert.notEqual(calls[1].ctx, calls[2].ctx);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The hub element — see hub/lb-hub.browser.ts and docs/testing.md.
|
|
3
|
-
*
|
|
4
|
-
* Tier 4. jsdom is evidence for what the hub does to the document it is
|
|
5
|
-
* already holding: which element an event came from, what request that
|
|
6
|
-
* produces, which attributes land where, and which page host a path selects.
|
|
7
|
-
* A real network and a real frame belong to the browser tier, which is not
|
|
8
|
-
* built.
|
|
9
|
-
*
|
|
10
|
-
* The hub registers itself as its module loads, so the window goes in first
|
|
11
|
-
* and the module is imported after it.
|
|
12
|
-
*/
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=lb-hub.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lb-hub.test.d.ts","sourceRoot":"","sources":["../../tests/lb-hub.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|