@streetui/testing 1.0.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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.cjs +240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StreetUI contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @streetui/testing
|
|
2
|
+
|
|
3
|
+
StreetUI testing utilities — test renderer, query helpers, assertions
|
|
4
|
+
|
|
5
|
+
Part of [StreetUI](https://github.com/streetui/streetui) — a semantic,
|
|
6
|
+
signal-based UI framework with its own reactivity and keyed DOM reconciler
|
|
7
|
+
(no virtual DOM).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @streetui/testing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import * as pkg from '@streetui/testing';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both ESM (`import`) and CommonJS (`require`) entry points are shipped, with
|
|
22
|
+
matching TypeScript declarations.
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
findAllByRole: () => findAllByRole,
|
|
24
|
+
findByRole: () => findByRole,
|
|
25
|
+
findByText: () => findByText,
|
|
26
|
+
flushUpdates: () => flushUpdates,
|
|
27
|
+
render: () => render,
|
|
28
|
+
renderOnce: () => renderOnce,
|
|
29
|
+
renderServerThenHydrate: () => renderServerThenHydrate,
|
|
30
|
+
waitFor: () => waitFor
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/test-renderer.ts
|
|
35
|
+
var import_core = require("@streetui/core");
|
|
36
|
+
var import_compiler = require("@streetui/compiler");
|
|
37
|
+
var import_dom = require("@streetui/dom");
|
|
38
|
+
var import_renderer = require("@streetui/renderer");
|
|
39
|
+
function render(app) {
|
|
40
|
+
const compiled = (0, import_compiler.compile)(app);
|
|
41
|
+
const container = document.createElement("div");
|
|
42
|
+
document.body.appendChild(container);
|
|
43
|
+
const renderer = (0, import_renderer.createRenderer)({ domAdapter: new import_dom.BrowserDOMAdapter() });
|
|
44
|
+
const handle = renderer.mount(compiled, container);
|
|
45
|
+
return {
|
|
46
|
+
container,
|
|
47
|
+
handle,
|
|
48
|
+
unmount() {
|
|
49
|
+
handle.unmount();
|
|
50
|
+
if (container.parentNode !== null) {
|
|
51
|
+
container.parentNode.removeChild(container);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
flush() {
|
|
55
|
+
handle.flush();
|
|
56
|
+
},
|
|
57
|
+
getByTag(tag) {
|
|
58
|
+
const el = container.querySelector(tag);
|
|
59
|
+
if (el === null) {
|
|
60
|
+
throw new Error(`[StreetUI Testing] Element <${tag}> not found in render output`);
|
|
61
|
+
}
|
|
62
|
+
return el;
|
|
63
|
+
},
|
|
64
|
+
getAllByTag(tag) {
|
|
65
|
+
return Array.from(container.querySelectorAll(tag));
|
|
66
|
+
},
|
|
67
|
+
getByText(text) {
|
|
68
|
+
const all = Array.from(container.querySelectorAll("*"));
|
|
69
|
+
const match = all.find(
|
|
70
|
+
(el) => el.children.length === 0 && el.textContent?.includes(text)
|
|
71
|
+
);
|
|
72
|
+
if (match === void 0) {
|
|
73
|
+
throw new Error(`[StreetUI Testing] No element with text "${text}" found`);
|
|
74
|
+
}
|
|
75
|
+
return match;
|
|
76
|
+
},
|
|
77
|
+
getAllByText(text) {
|
|
78
|
+
return Array.from(container.querySelectorAll("*")).filter(
|
|
79
|
+
(el) => el.textContent?.includes(text)
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
query(selector) {
|
|
83
|
+
return container.querySelector(selector);
|
|
84
|
+
},
|
|
85
|
+
queryAll(selector) {
|
|
86
|
+
return Array.from(container.querySelectorAll(selector));
|
|
87
|
+
},
|
|
88
|
+
find(selector) {
|
|
89
|
+
const el = container.querySelector(selector);
|
|
90
|
+
if (el === null) {
|
|
91
|
+
throw new Error(`[StreetUI Testing] Selector "${selector}" matched nothing`);
|
|
92
|
+
}
|
|
93
|
+
return el;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function renderOnce(app, testFn) {
|
|
98
|
+
const result = render(app);
|
|
99
|
+
return Promise.resolve(testFn(result)).finally(() => {
|
|
100
|
+
result.unmount();
|
|
101
|
+
(0, import_core.resetIdCounter)();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/helpers.ts
|
|
106
|
+
var import_core2 = require("@streetui/core");
|
|
107
|
+
var import_scheduler = require("@streetui/scheduler");
|
|
108
|
+
var import_compiler2 = require("@streetui/compiler");
|
|
109
|
+
var import_dom2 = require("@streetui/dom");
|
|
110
|
+
var import_renderer2 = require("@streetui/renderer");
|
|
111
|
+
async function flushUpdates() {
|
|
112
|
+
(0, import_scheduler.flushSync)();
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
(0, import_scheduler.flushSync)();
|
|
115
|
+
}
|
|
116
|
+
async function waitFor(check, options = {}) {
|
|
117
|
+
const timeout = options.timeout ?? 1e3;
|
|
118
|
+
const interval = options.interval ?? 10;
|
|
119
|
+
const deadline = Date.now() + timeout;
|
|
120
|
+
let lastError;
|
|
121
|
+
for (; ; ) {
|
|
122
|
+
await flushUpdates();
|
|
123
|
+
try {
|
|
124
|
+
const result = check();
|
|
125
|
+
if (result) return result;
|
|
126
|
+
lastError = new Error("[StreetUI Testing] waitFor: condition was falsy");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
lastError = err;
|
|
129
|
+
}
|
|
130
|
+
if (Date.now() >= deadline) {
|
|
131
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
132
|
+
}
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function findByText(container, text) {
|
|
137
|
+
const match = Array.from(container.querySelectorAll("*")).find(
|
|
138
|
+
(el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false)
|
|
139
|
+
);
|
|
140
|
+
if (match === void 0) {
|
|
141
|
+
throw new Error(`[StreetUI Testing] No element with text "${text}" found`);
|
|
142
|
+
}
|
|
143
|
+
return match;
|
|
144
|
+
}
|
|
145
|
+
function implicitRole(el) {
|
|
146
|
+
const tag = el.tagName.toLowerCase();
|
|
147
|
+
switch (tag) {
|
|
148
|
+
case "button":
|
|
149
|
+
return "button";
|
|
150
|
+
case "a":
|
|
151
|
+
return el.hasAttribute("href") ? "link" : null;
|
|
152
|
+
case "nav":
|
|
153
|
+
return "navigation";
|
|
154
|
+
case "h1":
|
|
155
|
+
case "h2":
|
|
156
|
+
case "h3":
|
|
157
|
+
case "h4":
|
|
158
|
+
case "h5":
|
|
159
|
+
case "h6":
|
|
160
|
+
return "heading";
|
|
161
|
+
case "input": {
|
|
162
|
+
const type = (el.getAttribute("type") ?? "text").toLowerCase();
|
|
163
|
+
if (type === "checkbox") return "checkbox";
|
|
164
|
+
if (type === "radio") return "radio";
|
|
165
|
+
if (type === "button" || type === "submit") return "button";
|
|
166
|
+
return "textbox";
|
|
167
|
+
}
|
|
168
|
+
case "form":
|
|
169
|
+
return "form";
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function accessibleName(el) {
|
|
175
|
+
return (el.getAttribute("aria-label") ?? el.textContent ?? "").trim();
|
|
176
|
+
}
|
|
177
|
+
function findAllByRole(container, role, options = {}) {
|
|
178
|
+
const all = Array.from(container.querySelectorAll("*"));
|
|
179
|
+
return all.filter((el) => {
|
|
180
|
+
const explicit = el.getAttribute("role");
|
|
181
|
+
const matches = explicit === role || explicit === null && implicitRole(el) === role;
|
|
182
|
+
if (!matches) return false;
|
|
183
|
+
if (options.name !== void 0) {
|
|
184
|
+
return accessibleName(el).includes(options.name);
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function findByRole(container, role, options = {}) {
|
|
190
|
+
const matches = findAllByRole(container, role, options);
|
|
191
|
+
if (matches.length === 0) {
|
|
192
|
+
const named = options.name !== void 0 ? ` with name "${options.name}"` : "";
|
|
193
|
+
throw new Error(`[StreetUI Testing] No element with role "${role}"${named} found`);
|
|
194
|
+
}
|
|
195
|
+
if (matches.length > 1) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[StreetUI Testing] Found ${matches.length} elements with role "${role}" \u2014 refine with { name }`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return matches[0];
|
|
201
|
+
}
|
|
202
|
+
function renderServerThenHydrate(build, options = {}) {
|
|
203
|
+
(0, import_core2.resetIdCounter)();
|
|
204
|
+
const serverHtml = (0, import_renderer2.renderToString)((0, import_compiler2.compile)(build()));
|
|
205
|
+
const container = document.createElement("div");
|
|
206
|
+
container.innerHTML = serverHtml;
|
|
207
|
+
document.body.appendChild(container);
|
|
208
|
+
const collector = options.collectDiagnostics === true ? (0, import_renderer2.createHydrationDiagnosticCollector)() : void 0;
|
|
209
|
+
(0, import_core2.resetIdCounter)();
|
|
210
|
+
const renderer = (0, import_renderer2.createRenderer)({
|
|
211
|
+
domAdapter: new import_dom2.BrowserDOMAdapter(),
|
|
212
|
+
...collector !== void 0 ? { hydrationDiagnostics: collector.sink } : {}
|
|
213
|
+
});
|
|
214
|
+
const handle = renderer.hydrate((0, import_compiler2.compile)(build()), container);
|
|
215
|
+
return {
|
|
216
|
+
container,
|
|
217
|
+
serverHtml,
|
|
218
|
+
handle,
|
|
219
|
+
diagnostics: collector?.diagnostics ?? [],
|
|
220
|
+
flush() {
|
|
221
|
+
handle.flush();
|
|
222
|
+
},
|
|
223
|
+
unmount() {
|
|
224
|
+
handle.unmount();
|
|
225
|
+
if (container.parentNode !== null) container.parentNode.removeChild(container);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
230
|
+
0 && (module.exports = {
|
|
231
|
+
findAllByRole,
|
|
232
|
+
findByRole,
|
|
233
|
+
findByText,
|
|
234
|
+
flushUpdates,
|
|
235
|
+
render,
|
|
236
|
+
renderOnce,
|
|
237
|
+
renderServerThenHydrate,
|
|
238
|
+
waitFor
|
|
239
|
+
});
|
|
240
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/test-renderer.ts","../src/helpers.ts"],"sourcesContent":["export * from './test-renderer.js';\nexport * from './helpers.js';\n","/**\n * StreetUI Test Renderer.\n *\n * Renders a StreetApp into a real (happy-dom / jsdom) DOM container\n * and exposes query helpers so tests can assert on structure/content\n * without importing the browser renderer directly.\n */\n\nimport { resetIdCounter } from '@streetui/core';\nimport { compile } from '@streetui/compiler';\nimport type { StreetApp } from '@streetui/dsl';\nimport { BrowserDOMAdapter } from '@streetui/dom';\nimport { createRenderer } from '@streetui/renderer';\nimport type { RenderHandle } from '@streetui/runtime';\n\nexport interface RenderResult {\n /** The root container element that was rendered into. */\n readonly container: HTMLElement;\n /** Unmount and clean up the render. */\n unmount(): void;\n /** Query a single element (throws if missing). */\n getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K];\n /** Query all elements by tag. */\n getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]>;\n /** Query by text content (partial match). */\n getByText(text: string): Element;\n /** Query all elements whose text content includes the given string. */\n getAllByText(text: string): Element[];\n /** Raw querySelector. */\n query(selector: string): Element | null;\n /** Raw querySelectorAll. */\n queryAll(selector: string): Element[];\n /** Assert element exists; return it. */\n find(selector: string): Element;\n /** Force a flush of any pending scheduler work. */\n flush(): void;\n /** The underlying render handle. */\n readonly handle: RenderHandle;\n}\n\n/**\n * Render a StreetApp into a detached DOM container.\n * Uses the real StreetUI renderer backed by happy-dom/jsdom.\n */\nexport function render(app: StreetApp): RenderResult {\n const compiled = compile(app);\n const container = document.createElement('div');\n document.body.appendChild(container);\n\n const renderer = createRenderer({ domAdapter: new BrowserDOMAdapter() });\n const handle = renderer.mount(compiled, container);\n\n return {\n container,\n handle,\n unmount() {\n handle.unmount();\n if (container.parentNode !== null) {\n container.parentNode.removeChild(container);\n }\n },\n flush() {\n handle.flush();\n },\n getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K] {\n const el = container.querySelector(tag);\n if (el === null) {\n throw new Error(`[StreetUI Testing] Element <${tag}> not found in render output`);\n }\n return el as HTMLElementTagNameMap[K];\n },\n getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]> {\n return Array.from(container.querySelectorAll(tag)) as Array<HTMLElementTagNameMap[K]>;\n },\n getByText(text: string): Element {\n const all = Array.from(container.querySelectorAll('*'));\n const match = all.find(el =>\n el.children.length === 0 && el.textContent?.includes(text),\n );\n if (match === undefined) {\n throw new Error(`[StreetUI Testing] No element with text \"${text}\" found`);\n }\n return match;\n },\n getAllByText(text: string): Element[] {\n return Array.from(container.querySelectorAll('*')).filter(el =>\n el.textContent?.includes(text),\n );\n },\n query(selector: string): Element | null {\n return container.querySelector(selector);\n },\n queryAll(selector: string): Element[] {\n return Array.from(container.querySelectorAll(selector));\n },\n find(selector: string): Element {\n const el = container.querySelector(selector);\n if (el === null) {\n throw new Error(`[StreetUI Testing] Selector \"${selector}\" matched nothing`);\n }\n return el;\n },\n };\n}\n\n/** Render and automatically clean up after the test. */\nexport function renderOnce(\n app: StreetApp,\n testFn: (result: RenderResult) => void | Promise<void>,\n): Promise<void> {\n const result = render(app);\n return Promise.resolve(testFn(result)).finally(() => {\n result.unmount();\n resetIdCounter();\n });\n}\n","/**\n * Higher-level testing helpers: role/text queries, update flushing, async\n * waiting, and a first-class SSR → hydrate → assert workflow.\n *\n * These build on the same real renderer the app uses — no private-graph access,\n * no second assertion framework. They exist so a test does not have to\n * reimplement server-render/hydrate plumbing or poll for async resource state\n * by hand.\n */\n\nimport { resetIdCounter } from '@streetui/core';\nimport { flushSync } from '@streetui/scheduler';\nimport { compile } from '@streetui/compiler';\nimport type { StreetApp } from '@streetui/dsl';\nimport { BrowserDOMAdapter } from '@streetui/dom';\nimport {\n createRenderer,\n renderToString,\n type HydrationDiagnostic,\n createHydrationDiagnosticCollector,\n} from '@streetui/renderer';\nimport type { RenderHandle } from '@streetui/runtime';\n\n// ── Update flushing & async waiting ─────────────────────────────────────────\n\n/**\n * Flush all pending scheduler work, then yield once to the microtask queue so\n * promise-driven updates (e.g. a resolved `resource` loader) are applied. Await\n * this after triggering a change that schedules a DOM patch.\n */\nexport async function flushUpdates(): Promise<void> {\n flushSync();\n await Promise.resolve();\n flushSync();\n}\n\nexport interface WaitForOptions {\n /** Give up after this many milliseconds (default 1000). */\n readonly timeout?: number;\n /** Delay between attempts in milliseconds (default 10). */\n readonly interval?: number;\n}\n\n/**\n * Poll `check` until it returns a truthy value (or stops throwing), flushing\n * updates between attempts. Rejects with the last error/tiemout after the\n * deadline. Use for assertions that become true only after async work settles.\n */\nexport async function waitFor<T>(\n check: () => T,\n options: WaitForOptions = {},\n): Promise<T> {\n const timeout = options.timeout ?? 1000;\n const interval = options.interval ?? 10;\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n for (;;) {\n await flushUpdates();\n try {\n const result = check();\n if (result) return result;\n lastError = new Error('[StreetUI Testing] waitFor: condition was falsy');\n } catch (err) {\n lastError = err;\n }\n if (Date.now() >= deadline) {\n throw lastError instanceof Error\n ? lastError\n : new Error(String(lastError));\n }\n await new Promise((resolve) => setTimeout(resolve, interval));\n }\n}\n\n// ── Text / role queries (container-scoped) ──────────────────────────────────\n\n/** Find the first leaf element whose text content includes `text`. */\nexport function findByText(container: Element, text: string): Element {\n const match = Array.from(container.querySelectorAll('*')).find(\n (el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false),\n );\n if (match === undefined) {\n throw new Error(`[StreetUI Testing] No element with text \"${text}\" found`);\n }\n return match;\n}\n\nexport interface ByRoleOptions {\n /** Restrict to elements whose accessible name includes this string. */\n readonly name?: string;\n}\n\n/** The implicit ARIA role for a plain HTML element, when it has one. */\nfunction implicitRole(el: Element): string | null {\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case 'button':\n return 'button';\n case 'a':\n return el.hasAttribute('href') ? 'link' : null;\n case 'nav':\n return 'navigation';\n case 'h1':\n case 'h2':\n case 'h3':\n case 'h4':\n case 'h5':\n case 'h6':\n return 'heading';\n case 'input': {\n const type = (el.getAttribute('type') ?? 'text').toLowerCase();\n if (type === 'checkbox') return 'checkbox';\n if (type === 'radio') return 'radio';\n if (type === 'button' || type === 'submit') return 'button';\n return 'textbox';\n }\n case 'form':\n return 'form';\n default:\n return null;\n }\n}\n\n/** The accessible name approximation for an element (aria-label or text). */\nfunction accessibleName(el: Element): string {\n return (el.getAttribute('aria-label') ?? el.textContent ?? '').trim();\n}\n\n/**\n * Find all elements matching an ARIA `role` — explicit `role=\"…\"` first, then\n * the element's implicit role. Optionally filter by accessible name.\n */\nexport function findAllByRole(\n container: Element,\n role: string,\n options: ByRoleOptions = {},\n): Element[] {\n const all = Array.from(container.querySelectorAll('*'));\n return all.filter((el) => {\n const explicit = el.getAttribute('role');\n const matches = explicit === role || (explicit === null && implicitRole(el) === role);\n if (!matches) return false;\n if (options.name !== undefined) {\n return accessibleName(el).includes(options.name);\n }\n return true;\n });\n}\n\n/** Find the single element matching a role (throws if none/ambiguous). */\nexport function findByRole(\n container: Element,\n role: string,\n options: ByRoleOptions = {},\n): Element {\n const matches = findAllByRole(container, role, options);\n if (matches.length === 0) {\n const named = options.name !== undefined ? ` with name \"${options.name}\"` : '';\n throw new Error(`[StreetUI Testing] No element with role \"${role}\"${named} found`);\n }\n if (matches.length > 1) {\n throw new Error(\n `[StreetUI Testing] Found ${matches.length} elements with role \"${role}\" — refine with { name }`,\n );\n }\n return matches[0]!;\n}\n\n// ── First-class SSR → hydrate → assert workflow ─────────────────────────────\n\nexport interface HydrateTestOptions {\n /** Collect hydration mismatch diagnostics (dev-style) during hydration. */\n readonly collectDiagnostics?: boolean;\n}\n\nexport interface HydrateTestResult {\n /** The container holding the server HTML, now hydrated live. */\n readonly container: HTMLElement;\n /** The server-produced HTML string (before hydration). */\n readonly serverHtml: string;\n /** The live render handle from hydration. */\n readonly handle: RenderHandle;\n /** Hydration mismatch diagnostics (empty unless collectDiagnostics + a real mismatch). */\n readonly diagnostics: readonly HydrationDiagnostic[];\n /** Flush pending scheduler work. */\n flush(): void;\n /** Unmount and detach the container. */\n unmount(): void;\n}\n\n/**\n * Render `build` on the \"server\" to HTML, mount that HTML into a container,\n * then hydrate the SAME app against it — exactly the production SSR path. The\n * builder is invoked twice (server then client) with `resetIdCounter` between,\n * so deterministic ids line up. Assert node identity, behavior, and (optionally)\n * that hydration reported no mismatches.\n */\nexport function renderServerThenHydrate(\n build: () => StreetApp,\n options: HydrateTestOptions = {},\n): HydrateTestResult {\n resetIdCounter();\n const serverHtml = renderToString(compile(build()));\n\n const container = document.createElement('div');\n container.innerHTML = serverHtml;\n document.body.appendChild(container);\n\n const collector = options.collectDiagnostics === true\n ? createHydrationDiagnosticCollector()\n : undefined;\n\n resetIdCounter();\n const renderer = createRenderer({\n domAdapter: new BrowserDOMAdapter(),\n ...(collector !== undefined ? { hydrationDiagnostics: collector.sink } : {}),\n });\n const handle = renderer.hydrate(compile(build()), container);\n\n return {\n container,\n serverHtml,\n handle,\n diagnostics: collector?.diagnostics ?? [],\n flush() {\n handle.flush();\n },\n unmount() {\n handle.unmount();\n if (container.parentNode !== null) container.parentNode.removeChild(container);\n },\n };\n}\n"],"mappings":"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","names":["import_core","import_compiler","import_dom","import_renderer"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { StreetApp } from '@streetui/dsl';
|
|
2
|
+
import { RenderHandle } from '@streetui/runtime';
|
|
3
|
+
import { HydrationDiagnostic } from '@streetui/renderer';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StreetUI Test Renderer.
|
|
7
|
+
*
|
|
8
|
+
* Renders a StreetApp into a real (happy-dom / jsdom) DOM container
|
|
9
|
+
* and exposes query helpers so tests can assert on structure/content
|
|
10
|
+
* without importing the browser renderer directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RenderResult {
|
|
14
|
+
/** The root container element that was rendered into. */
|
|
15
|
+
readonly container: HTMLElement;
|
|
16
|
+
/** Unmount and clean up the render. */
|
|
17
|
+
unmount(): void;
|
|
18
|
+
/** Query a single element (throws if missing). */
|
|
19
|
+
getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K];
|
|
20
|
+
/** Query all elements by tag. */
|
|
21
|
+
getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]>;
|
|
22
|
+
/** Query by text content (partial match). */
|
|
23
|
+
getByText(text: string): Element;
|
|
24
|
+
/** Query all elements whose text content includes the given string. */
|
|
25
|
+
getAllByText(text: string): Element[];
|
|
26
|
+
/** Raw querySelector. */
|
|
27
|
+
query(selector: string): Element | null;
|
|
28
|
+
/** Raw querySelectorAll. */
|
|
29
|
+
queryAll(selector: string): Element[];
|
|
30
|
+
/** Assert element exists; return it. */
|
|
31
|
+
find(selector: string): Element;
|
|
32
|
+
/** Force a flush of any pending scheduler work. */
|
|
33
|
+
flush(): void;
|
|
34
|
+
/** The underlying render handle. */
|
|
35
|
+
readonly handle: RenderHandle;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render a StreetApp into a detached DOM container.
|
|
39
|
+
* Uses the real StreetUI renderer backed by happy-dom/jsdom.
|
|
40
|
+
*/
|
|
41
|
+
declare function render(app: StreetApp): RenderResult;
|
|
42
|
+
/** Render and automatically clean up after the test. */
|
|
43
|
+
declare function renderOnce(app: StreetApp, testFn: (result: RenderResult) => void | Promise<void>): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Higher-level testing helpers: role/text queries, update flushing, async
|
|
47
|
+
* waiting, and a first-class SSR → hydrate → assert workflow.
|
|
48
|
+
*
|
|
49
|
+
* These build on the same real renderer the app uses — no private-graph access,
|
|
50
|
+
* no second assertion framework. They exist so a test does not have to
|
|
51
|
+
* reimplement server-render/hydrate plumbing or poll for async resource state
|
|
52
|
+
* by hand.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Flush all pending scheduler work, then yield once to the microtask queue so
|
|
57
|
+
* promise-driven updates (e.g. a resolved `resource` loader) are applied. Await
|
|
58
|
+
* this after triggering a change that schedules a DOM patch.
|
|
59
|
+
*/
|
|
60
|
+
declare function flushUpdates(): Promise<void>;
|
|
61
|
+
interface WaitForOptions {
|
|
62
|
+
/** Give up after this many milliseconds (default 1000). */
|
|
63
|
+
readonly timeout?: number;
|
|
64
|
+
/** Delay between attempts in milliseconds (default 10). */
|
|
65
|
+
readonly interval?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Poll `check` until it returns a truthy value (or stops throwing), flushing
|
|
69
|
+
* updates between attempts. Rejects with the last error/tiemout after the
|
|
70
|
+
* deadline. Use for assertions that become true only after async work settles.
|
|
71
|
+
*/
|
|
72
|
+
declare function waitFor<T>(check: () => T, options?: WaitForOptions): Promise<T>;
|
|
73
|
+
/** Find the first leaf element whose text content includes `text`. */
|
|
74
|
+
declare function findByText(container: Element, text: string): Element;
|
|
75
|
+
interface ByRoleOptions {
|
|
76
|
+
/** Restrict to elements whose accessible name includes this string. */
|
|
77
|
+
readonly name?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Find all elements matching an ARIA `role` — explicit `role="…"` first, then
|
|
81
|
+
* the element's implicit role. Optionally filter by accessible name.
|
|
82
|
+
*/
|
|
83
|
+
declare function findAllByRole(container: Element, role: string, options?: ByRoleOptions): Element[];
|
|
84
|
+
/** Find the single element matching a role (throws if none/ambiguous). */
|
|
85
|
+
declare function findByRole(container: Element, role: string, options?: ByRoleOptions): Element;
|
|
86
|
+
interface HydrateTestOptions {
|
|
87
|
+
/** Collect hydration mismatch diagnostics (dev-style) during hydration. */
|
|
88
|
+
readonly collectDiagnostics?: boolean;
|
|
89
|
+
}
|
|
90
|
+
interface HydrateTestResult {
|
|
91
|
+
/** The container holding the server HTML, now hydrated live. */
|
|
92
|
+
readonly container: HTMLElement;
|
|
93
|
+
/** The server-produced HTML string (before hydration). */
|
|
94
|
+
readonly serverHtml: string;
|
|
95
|
+
/** The live render handle from hydration. */
|
|
96
|
+
readonly handle: RenderHandle;
|
|
97
|
+
/** Hydration mismatch diagnostics (empty unless collectDiagnostics + a real mismatch). */
|
|
98
|
+
readonly diagnostics: readonly HydrationDiagnostic[];
|
|
99
|
+
/** Flush pending scheduler work. */
|
|
100
|
+
flush(): void;
|
|
101
|
+
/** Unmount and detach the container. */
|
|
102
|
+
unmount(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Render `build` on the "server" to HTML, mount that HTML into a container,
|
|
106
|
+
* then hydrate the SAME app against it — exactly the production SSR path. The
|
|
107
|
+
* builder is invoked twice (server then client) with `resetIdCounter` between,
|
|
108
|
+
* so deterministic ids line up. Assert node identity, behavior, and (optionally)
|
|
109
|
+
* that hydration reported no mismatches.
|
|
110
|
+
*/
|
|
111
|
+
declare function renderServerThenHydrate(build: () => StreetApp, options?: HydrateTestOptions): HydrateTestResult;
|
|
112
|
+
|
|
113
|
+
export { type ByRoleOptions, type HydrateTestOptions, type HydrateTestResult, type RenderResult, type WaitForOptions, findAllByRole, findByRole, findByText, flushUpdates, render, renderOnce, renderServerThenHydrate, waitFor };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { StreetApp } from '@streetui/dsl';
|
|
2
|
+
import { RenderHandle } from '@streetui/runtime';
|
|
3
|
+
import { HydrationDiagnostic } from '@streetui/renderer';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StreetUI Test Renderer.
|
|
7
|
+
*
|
|
8
|
+
* Renders a StreetApp into a real (happy-dom / jsdom) DOM container
|
|
9
|
+
* and exposes query helpers so tests can assert on structure/content
|
|
10
|
+
* without importing the browser renderer directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RenderResult {
|
|
14
|
+
/** The root container element that was rendered into. */
|
|
15
|
+
readonly container: HTMLElement;
|
|
16
|
+
/** Unmount and clean up the render. */
|
|
17
|
+
unmount(): void;
|
|
18
|
+
/** Query a single element (throws if missing). */
|
|
19
|
+
getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K];
|
|
20
|
+
/** Query all elements by tag. */
|
|
21
|
+
getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]>;
|
|
22
|
+
/** Query by text content (partial match). */
|
|
23
|
+
getByText(text: string): Element;
|
|
24
|
+
/** Query all elements whose text content includes the given string. */
|
|
25
|
+
getAllByText(text: string): Element[];
|
|
26
|
+
/** Raw querySelector. */
|
|
27
|
+
query(selector: string): Element | null;
|
|
28
|
+
/** Raw querySelectorAll. */
|
|
29
|
+
queryAll(selector: string): Element[];
|
|
30
|
+
/** Assert element exists; return it. */
|
|
31
|
+
find(selector: string): Element;
|
|
32
|
+
/** Force a flush of any pending scheduler work. */
|
|
33
|
+
flush(): void;
|
|
34
|
+
/** The underlying render handle. */
|
|
35
|
+
readonly handle: RenderHandle;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render a StreetApp into a detached DOM container.
|
|
39
|
+
* Uses the real StreetUI renderer backed by happy-dom/jsdom.
|
|
40
|
+
*/
|
|
41
|
+
declare function render(app: StreetApp): RenderResult;
|
|
42
|
+
/** Render and automatically clean up after the test. */
|
|
43
|
+
declare function renderOnce(app: StreetApp, testFn: (result: RenderResult) => void | Promise<void>): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Higher-level testing helpers: role/text queries, update flushing, async
|
|
47
|
+
* waiting, and a first-class SSR → hydrate → assert workflow.
|
|
48
|
+
*
|
|
49
|
+
* These build on the same real renderer the app uses — no private-graph access,
|
|
50
|
+
* no second assertion framework. They exist so a test does not have to
|
|
51
|
+
* reimplement server-render/hydrate plumbing or poll for async resource state
|
|
52
|
+
* by hand.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Flush all pending scheduler work, then yield once to the microtask queue so
|
|
57
|
+
* promise-driven updates (e.g. a resolved `resource` loader) are applied. Await
|
|
58
|
+
* this after triggering a change that schedules a DOM patch.
|
|
59
|
+
*/
|
|
60
|
+
declare function flushUpdates(): Promise<void>;
|
|
61
|
+
interface WaitForOptions {
|
|
62
|
+
/** Give up after this many milliseconds (default 1000). */
|
|
63
|
+
readonly timeout?: number;
|
|
64
|
+
/** Delay between attempts in milliseconds (default 10). */
|
|
65
|
+
readonly interval?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Poll `check` until it returns a truthy value (or stops throwing), flushing
|
|
69
|
+
* updates between attempts. Rejects with the last error/tiemout after the
|
|
70
|
+
* deadline. Use for assertions that become true only after async work settles.
|
|
71
|
+
*/
|
|
72
|
+
declare function waitFor<T>(check: () => T, options?: WaitForOptions): Promise<T>;
|
|
73
|
+
/** Find the first leaf element whose text content includes `text`. */
|
|
74
|
+
declare function findByText(container: Element, text: string): Element;
|
|
75
|
+
interface ByRoleOptions {
|
|
76
|
+
/** Restrict to elements whose accessible name includes this string. */
|
|
77
|
+
readonly name?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Find all elements matching an ARIA `role` — explicit `role="…"` first, then
|
|
81
|
+
* the element's implicit role. Optionally filter by accessible name.
|
|
82
|
+
*/
|
|
83
|
+
declare function findAllByRole(container: Element, role: string, options?: ByRoleOptions): Element[];
|
|
84
|
+
/** Find the single element matching a role (throws if none/ambiguous). */
|
|
85
|
+
declare function findByRole(container: Element, role: string, options?: ByRoleOptions): Element;
|
|
86
|
+
interface HydrateTestOptions {
|
|
87
|
+
/** Collect hydration mismatch diagnostics (dev-style) during hydration. */
|
|
88
|
+
readonly collectDiagnostics?: boolean;
|
|
89
|
+
}
|
|
90
|
+
interface HydrateTestResult {
|
|
91
|
+
/** The container holding the server HTML, now hydrated live. */
|
|
92
|
+
readonly container: HTMLElement;
|
|
93
|
+
/** The server-produced HTML string (before hydration). */
|
|
94
|
+
readonly serverHtml: string;
|
|
95
|
+
/** The live render handle from hydration. */
|
|
96
|
+
readonly handle: RenderHandle;
|
|
97
|
+
/** Hydration mismatch diagnostics (empty unless collectDiagnostics + a real mismatch). */
|
|
98
|
+
readonly diagnostics: readonly HydrationDiagnostic[];
|
|
99
|
+
/** Flush pending scheduler work. */
|
|
100
|
+
flush(): void;
|
|
101
|
+
/** Unmount and detach the container. */
|
|
102
|
+
unmount(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Render `build` on the "server" to HTML, mount that HTML into a container,
|
|
106
|
+
* then hydrate the SAME app against it — exactly the production SSR path. The
|
|
107
|
+
* builder is invoked twice (server then client) with `resetIdCounter` between,
|
|
108
|
+
* so deterministic ids line up. Assert node identity, behavior, and (optionally)
|
|
109
|
+
* that hydration reported no mismatches.
|
|
110
|
+
*/
|
|
111
|
+
declare function renderServerThenHydrate(build: () => StreetApp, options?: HydrateTestOptions): HydrateTestResult;
|
|
112
|
+
|
|
113
|
+
export { type ByRoleOptions, type HydrateTestOptions, type HydrateTestResult, type RenderResult, type WaitForOptions, findAllByRole, findByRole, findByText, flushUpdates, render, renderOnce, renderServerThenHydrate, waitFor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/test-renderer.ts
|
|
2
|
+
import { resetIdCounter } from "@streetui/core";
|
|
3
|
+
import { compile } from "@streetui/compiler";
|
|
4
|
+
import { BrowserDOMAdapter } from "@streetui/dom";
|
|
5
|
+
import { createRenderer } from "@streetui/renderer";
|
|
6
|
+
function render(app) {
|
|
7
|
+
const compiled = compile(app);
|
|
8
|
+
const container = document.createElement("div");
|
|
9
|
+
document.body.appendChild(container);
|
|
10
|
+
const renderer = createRenderer({ domAdapter: new BrowserDOMAdapter() });
|
|
11
|
+
const handle = renderer.mount(compiled, container);
|
|
12
|
+
return {
|
|
13
|
+
container,
|
|
14
|
+
handle,
|
|
15
|
+
unmount() {
|
|
16
|
+
handle.unmount();
|
|
17
|
+
if (container.parentNode !== null) {
|
|
18
|
+
container.parentNode.removeChild(container);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
flush() {
|
|
22
|
+
handle.flush();
|
|
23
|
+
},
|
|
24
|
+
getByTag(tag) {
|
|
25
|
+
const el = container.querySelector(tag);
|
|
26
|
+
if (el === null) {
|
|
27
|
+
throw new Error(`[StreetUI Testing] Element <${tag}> not found in render output`);
|
|
28
|
+
}
|
|
29
|
+
return el;
|
|
30
|
+
},
|
|
31
|
+
getAllByTag(tag) {
|
|
32
|
+
return Array.from(container.querySelectorAll(tag));
|
|
33
|
+
},
|
|
34
|
+
getByText(text) {
|
|
35
|
+
const all = Array.from(container.querySelectorAll("*"));
|
|
36
|
+
const match = all.find(
|
|
37
|
+
(el) => el.children.length === 0 && el.textContent?.includes(text)
|
|
38
|
+
);
|
|
39
|
+
if (match === void 0) {
|
|
40
|
+
throw new Error(`[StreetUI Testing] No element with text "${text}" found`);
|
|
41
|
+
}
|
|
42
|
+
return match;
|
|
43
|
+
},
|
|
44
|
+
getAllByText(text) {
|
|
45
|
+
return Array.from(container.querySelectorAll("*")).filter(
|
|
46
|
+
(el) => el.textContent?.includes(text)
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
query(selector) {
|
|
50
|
+
return container.querySelector(selector);
|
|
51
|
+
},
|
|
52
|
+
queryAll(selector) {
|
|
53
|
+
return Array.from(container.querySelectorAll(selector));
|
|
54
|
+
},
|
|
55
|
+
find(selector) {
|
|
56
|
+
const el = container.querySelector(selector);
|
|
57
|
+
if (el === null) {
|
|
58
|
+
throw new Error(`[StreetUI Testing] Selector "${selector}" matched nothing`);
|
|
59
|
+
}
|
|
60
|
+
return el;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function renderOnce(app, testFn) {
|
|
65
|
+
const result = render(app);
|
|
66
|
+
return Promise.resolve(testFn(result)).finally(() => {
|
|
67
|
+
result.unmount();
|
|
68
|
+
resetIdCounter();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/helpers.ts
|
|
73
|
+
import { resetIdCounter as resetIdCounter2 } from "@streetui/core";
|
|
74
|
+
import { flushSync } from "@streetui/scheduler";
|
|
75
|
+
import { compile as compile2 } from "@streetui/compiler";
|
|
76
|
+
import { BrowserDOMAdapter as BrowserDOMAdapter2 } from "@streetui/dom";
|
|
77
|
+
import {
|
|
78
|
+
createRenderer as createRenderer2,
|
|
79
|
+
renderToString,
|
|
80
|
+
createHydrationDiagnosticCollector
|
|
81
|
+
} from "@streetui/renderer";
|
|
82
|
+
async function flushUpdates() {
|
|
83
|
+
flushSync();
|
|
84
|
+
await Promise.resolve();
|
|
85
|
+
flushSync();
|
|
86
|
+
}
|
|
87
|
+
async function waitFor(check, options = {}) {
|
|
88
|
+
const timeout = options.timeout ?? 1e3;
|
|
89
|
+
const interval = options.interval ?? 10;
|
|
90
|
+
const deadline = Date.now() + timeout;
|
|
91
|
+
let lastError;
|
|
92
|
+
for (; ; ) {
|
|
93
|
+
await flushUpdates();
|
|
94
|
+
try {
|
|
95
|
+
const result = check();
|
|
96
|
+
if (result) return result;
|
|
97
|
+
lastError = new Error("[StreetUI Testing] waitFor: condition was falsy");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
lastError = err;
|
|
100
|
+
}
|
|
101
|
+
if (Date.now() >= deadline) {
|
|
102
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
103
|
+
}
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function findByText(container, text) {
|
|
108
|
+
const match = Array.from(container.querySelectorAll("*")).find(
|
|
109
|
+
(el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false)
|
|
110
|
+
);
|
|
111
|
+
if (match === void 0) {
|
|
112
|
+
throw new Error(`[StreetUI Testing] No element with text "${text}" found`);
|
|
113
|
+
}
|
|
114
|
+
return match;
|
|
115
|
+
}
|
|
116
|
+
function implicitRole(el) {
|
|
117
|
+
const tag = el.tagName.toLowerCase();
|
|
118
|
+
switch (tag) {
|
|
119
|
+
case "button":
|
|
120
|
+
return "button";
|
|
121
|
+
case "a":
|
|
122
|
+
return el.hasAttribute("href") ? "link" : null;
|
|
123
|
+
case "nav":
|
|
124
|
+
return "navigation";
|
|
125
|
+
case "h1":
|
|
126
|
+
case "h2":
|
|
127
|
+
case "h3":
|
|
128
|
+
case "h4":
|
|
129
|
+
case "h5":
|
|
130
|
+
case "h6":
|
|
131
|
+
return "heading";
|
|
132
|
+
case "input": {
|
|
133
|
+
const type = (el.getAttribute("type") ?? "text").toLowerCase();
|
|
134
|
+
if (type === "checkbox") return "checkbox";
|
|
135
|
+
if (type === "radio") return "radio";
|
|
136
|
+
if (type === "button" || type === "submit") return "button";
|
|
137
|
+
return "textbox";
|
|
138
|
+
}
|
|
139
|
+
case "form":
|
|
140
|
+
return "form";
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function accessibleName(el) {
|
|
146
|
+
return (el.getAttribute("aria-label") ?? el.textContent ?? "").trim();
|
|
147
|
+
}
|
|
148
|
+
function findAllByRole(container, role, options = {}) {
|
|
149
|
+
const all = Array.from(container.querySelectorAll("*"));
|
|
150
|
+
return all.filter((el) => {
|
|
151
|
+
const explicit = el.getAttribute("role");
|
|
152
|
+
const matches = explicit === role || explicit === null && implicitRole(el) === role;
|
|
153
|
+
if (!matches) return false;
|
|
154
|
+
if (options.name !== void 0) {
|
|
155
|
+
return accessibleName(el).includes(options.name);
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function findByRole(container, role, options = {}) {
|
|
161
|
+
const matches = findAllByRole(container, role, options);
|
|
162
|
+
if (matches.length === 0) {
|
|
163
|
+
const named = options.name !== void 0 ? ` with name "${options.name}"` : "";
|
|
164
|
+
throw new Error(`[StreetUI Testing] No element with role "${role}"${named} found`);
|
|
165
|
+
}
|
|
166
|
+
if (matches.length > 1) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`[StreetUI Testing] Found ${matches.length} elements with role "${role}" \u2014 refine with { name }`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return matches[0];
|
|
172
|
+
}
|
|
173
|
+
function renderServerThenHydrate(build, options = {}) {
|
|
174
|
+
resetIdCounter2();
|
|
175
|
+
const serverHtml = renderToString(compile2(build()));
|
|
176
|
+
const container = document.createElement("div");
|
|
177
|
+
container.innerHTML = serverHtml;
|
|
178
|
+
document.body.appendChild(container);
|
|
179
|
+
const collector = options.collectDiagnostics === true ? createHydrationDiagnosticCollector() : void 0;
|
|
180
|
+
resetIdCounter2();
|
|
181
|
+
const renderer = createRenderer2({
|
|
182
|
+
domAdapter: new BrowserDOMAdapter2(),
|
|
183
|
+
...collector !== void 0 ? { hydrationDiagnostics: collector.sink } : {}
|
|
184
|
+
});
|
|
185
|
+
const handle = renderer.hydrate(compile2(build()), container);
|
|
186
|
+
return {
|
|
187
|
+
container,
|
|
188
|
+
serverHtml,
|
|
189
|
+
handle,
|
|
190
|
+
diagnostics: collector?.diagnostics ?? [],
|
|
191
|
+
flush() {
|
|
192
|
+
handle.flush();
|
|
193
|
+
},
|
|
194
|
+
unmount() {
|
|
195
|
+
handle.unmount();
|
|
196
|
+
if (container.parentNode !== null) container.parentNode.removeChild(container);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
findAllByRole,
|
|
202
|
+
findByRole,
|
|
203
|
+
findByText,
|
|
204
|
+
flushUpdates,
|
|
205
|
+
render,
|
|
206
|
+
renderOnce,
|
|
207
|
+
renderServerThenHydrate,
|
|
208
|
+
waitFor
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/test-renderer.ts","../src/helpers.ts"],"sourcesContent":["/**\n * StreetUI Test Renderer.\n *\n * Renders a StreetApp into a real (happy-dom / jsdom) DOM container\n * and exposes query helpers so tests can assert on structure/content\n * without importing the browser renderer directly.\n */\n\nimport { resetIdCounter } from '@streetui/core';\nimport { compile } from '@streetui/compiler';\nimport type { StreetApp } from '@streetui/dsl';\nimport { BrowserDOMAdapter } from '@streetui/dom';\nimport { createRenderer } from '@streetui/renderer';\nimport type { RenderHandle } from '@streetui/runtime';\n\nexport interface RenderResult {\n /** The root container element that was rendered into. */\n readonly container: HTMLElement;\n /** Unmount and clean up the render. */\n unmount(): void;\n /** Query a single element (throws if missing). */\n getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K];\n /** Query all elements by tag. */\n getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]>;\n /** Query by text content (partial match). */\n getByText(text: string): Element;\n /** Query all elements whose text content includes the given string. */\n getAllByText(text: string): Element[];\n /** Raw querySelector. */\n query(selector: string): Element | null;\n /** Raw querySelectorAll. */\n queryAll(selector: string): Element[];\n /** Assert element exists; return it. */\n find(selector: string): Element;\n /** Force a flush of any pending scheduler work. */\n flush(): void;\n /** The underlying render handle. */\n readonly handle: RenderHandle;\n}\n\n/**\n * Render a StreetApp into a detached DOM container.\n * Uses the real StreetUI renderer backed by happy-dom/jsdom.\n */\nexport function render(app: StreetApp): RenderResult {\n const compiled = compile(app);\n const container = document.createElement('div');\n document.body.appendChild(container);\n\n const renderer = createRenderer({ domAdapter: new BrowserDOMAdapter() });\n const handle = renderer.mount(compiled, container);\n\n return {\n container,\n handle,\n unmount() {\n handle.unmount();\n if (container.parentNode !== null) {\n container.parentNode.removeChild(container);\n }\n },\n flush() {\n handle.flush();\n },\n getByTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K] {\n const el = container.querySelector(tag);\n if (el === null) {\n throw new Error(`[StreetUI Testing] Element <${tag}> not found in render output`);\n }\n return el as HTMLElementTagNameMap[K];\n },\n getAllByTag<K extends keyof HTMLElementTagNameMap>(tag: K): Array<HTMLElementTagNameMap[K]> {\n return Array.from(container.querySelectorAll(tag)) as Array<HTMLElementTagNameMap[K]>;\n },\n getByText(text: string): Element {\n const all = Array.from(container.querySelectorAll('*'));\n const match = all.find(el =>\n el.children.length === 0 && el.textContent?.includes(text),\n );\n if (match === undefined) {\n throw new Error(`[StreetUI Testing] No element with text \"${text}\" found`);\n }\n return match;\n },\n getAllByText(text: string): Element[] {\n return Array.from(container.querySelectorAll('*')).filter(el =>\n el.textContent?.includes(text),\n );\n },\n query(selector: string): Element | null {\n return container.querySelector(selector);\n },\n queryAll(selector: string): Element[] {\n return Array.from(container.querySelectorAll(selector));\n },\n find(selector: string): Element {\n const el = container.querySelector(selector);\n if (el === null) {\n throw new Error(`[StreetUI Testing] Selector \"${selector}\" matched nothing`);\n }\n return el;\n },\n };\n}\n\n/** Render and automatically clean up after the test. */\nexport function renderOnce(\n app: StreetApp,\n testFn: (result: RenderResult) => void | Promise<void>,\n): Promise<void> {\n const result = render(app);\n return Promise.resolve(testFn(result)).finally(() => {\n result.unmount();\n resetIdCounter();\n });\n}\n","/**\n * Higher-level testing helpers: role/text queries, update flushing, async\n * waiting, and a first-class SSR → hydrate → assert workflow.\n *\n * These build on the same real renderer the app uses — no private-graph access,\n * no second assertion framework. They exist so a test does not have to\n * reimplement server-render/hydrate plumbing or poll for async resource state\n * by hand.\n */\n\nimport { resetIdCounter } from '@streetui/core';\nimport { flushSync } from '@streetui/scheduler';\nimport { compile } from '@streetui/compiler';\nimport type { StreetApp } from '@streetui/dsl';\nimport { BrowserDOMAdapter } from '@streetui/dom';\nimport {\n createRenderer,\n renderToString,\n type HydrationDiagnostic,\n createHydrationDiagnosticCollector,\n} from '@streetui/renderer';\nimport type { RenderHandle } from '@streetui/runtime';\n\n// ── Update flushing & async waiting ─────────────────────────────────────────\n\n/**\n * Flush all pending scheduler work, then yield once to the microtask queue so\n * promise-driven updates (e.g. a resolved `resource` loader) are applied. Await\n * this after triggering a change that schedules a DOM patch.\n */\nexport async function flushUpdates(): Promise<void> {\n flushSync();\n await Promise.resolve();\n flushSync();\n}\n\nexport interface WaitForOptions {\n /** Give up after this many milliseconds (default 1000). */\n readonly timeout?: number;\n /** Delay between attempts in milliseconds (default 10). */\n readonly interval?: number;\n}\n\n/**\n * Poll `check` until it returns a truthy value (or stops throwing), flushing\n * updates between attempts. Rejects with the last error/tiemout after the\n * deadline. Use for assertions that become true only after async work settles.\n */\nexport async function waitFor<T>(\n check: () => T,\n options: WaitForOptions = {},\n): Promise<T> {\n const timeout = options.timeout ?? 1000;\n const interval = options.interval ?? 10;\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n for (;;) {\n await flushUpdates();\n try {\n const result = check();\n if (result) return result;\n lastError = new Error('[StreetUI Testing] waitFor: condition was falsy');\n } catch (err) {\n lastError = err;\n }\n if (Date.now() >= deadline) {\n throw lastError instanceof Error\n ? lastError\n : new Error(String(lastError));\n }\n await new Promise((resolve) => setTimeout(resolve, interval));\n }\n}\n\n// ── Text / role queries (container-scoped) ──────────────────────────────────\n\n/** Find the first leaf element whose text content includes `text`. */\nexport function findByText(container: Element, text: string): Element {\n const match = Array.from(container.querySelectorAll('*')).find(\n (el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false),\n );\n if (match === undefined) {\n throw new Error(`[StreetUI Testing] No element with text \"${text}\" found`);\n }\n return match;\n}\n\nexport interface ByRoleOptions {\n /** Restrict to elements whose accessible name includes this string. */\n readonly name?: string;\n}\n\n/** The implicit ARIA role for a plain HTML element, when it has one. */\nfunction implicitRole(el: Element): string | null {\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case 'button':\n return 'button';\n case 'a':\n return el.hasAttribute('href') ? 'link' : null;\n case 'nav':\n return 'navigation';\n case 'h1':\n case 'h2':\n case 'h3':\n case 'h4':\n case 'h5':\n case 'h6':\n return 'heading';\n case 'input': {\n const type = (el.getAttribute('type') ?? 'text').toLowerCase();\n if (type === 'checkbox') return 'checkbox';\n if (type === 'radio') return 'radio';\n if (type === 'button' || type === 'submit') return 'button';\n return 'textbox';\n }\n case 'form':\n return 'form';\n default:\n return null;\n }\n}\n\n/** The accessible name approximation for an element (aria-label or text). */\nfunction accessibleName(el: Element): string {\n return (el.getAttribute('aria-label') ?? el.textContent ?? '').trim();\n}\n\n/**\n * Find all elements matching an ARIA `role` — explicit `role=\"…\"` first, then\n * the element's implicit role. Optionally filter by accessible name.\n */\nexport function findAllByRole(\n container: Element,\n role: string,\n options: ByRoleOptions = {},\n): Element[] {\n const all = Array.from(container.querySelectorAll('*'));\n return all.filter((el) => {\n const explicit = el.getAttribute('role');\n const matches = explicit === role || (explicit === null && implicitRole(el) === role);\n if (!matches) return false;\n if (options.name !== undefined) {\n return accessibleName(el).includes(options.name);\n }\n return true;\n });\n}\n\n/** Find the single element matching a role (throws if none/ambiguous). */\nexport function findByRole(\n container: Element,\n role: string,\n options: ByRoleOptions = {},\n): Element {\n const matches = findAllByRole(container, role, options);\n if (matches.length === 0) {\n const named = options.name !== undefined ? ` with name \"${options.name}\"` : '';\n throw new Error(`[StreetUI Testing] No element with role \"${role}\"${named} found`);\n }\n if (matches.length > 1) {\n throw new Error(\n `[StreetUI Testing] Found ${matches.length} elements with role \"${role}\" — refine with { name }`,\n );\n }\n return matches[0]!;\n}\n\n// ── First-class SSR → hydrate → assert workflow ─────────────────────────────\n\nexport interface HydrateTestOptions {\n /** Collect hydration mismatch diagnostics (dev-style) during hydration. */\n readonly collectDiagnostics?: boolean;\n}\n\nexport interface HydrateTestResult {\n /** The container holding the server HTML, now hydrated live. */\n readonly container: HTMLElement;\n /** The server-produced HTML string (before hydration). */\n readonly serverHtml: string;\n /** The live render handle from hydration. */\n readonly handle: RenderHandle;\n /** Hydration mismatch diagnostics (empty unless collectDiagnostics + a real mismatch). */\n readonly diagnostics: readonly HydrationDiagnostic[];\n /** Flush pending scheduler work. */\n flush(): void;\n /** Unmount and detach the container. */\n unmount(): void;\n}\n\n/**\n * Render `build` on the \"server\" to HTML, mount that HTML into a container,\n * then hydrate the SAME app against it — exactly the production SSR path. The\n * builder is invoked twice (server then client) with `resetIdCounter` between,\n * so deterministic ids line up. Assert node identity, behavior, and (optionally)\n * that hydration reported no mismatches.\n */\nexport function renderServerThenHydrate(\n build: () => StreetApp,\n options: HydrateTestOptions = {},\n): HydrateTestResult {\n resetIdCounter();\n const serverHtml = renderToString(compile(build()));\n\n const container = document.createElement('div');\n container.innerHTML = serverHtml;\n document.body.appendChild(container);\n\n const collector = options.collectDiagnostics === true\n ? createHydrationDiagnosticCollector()\n : undefined;\n\n resetIdCounter();\n const renderer = createRenderer({\n domAdapter: new BrowserDOMAdapter(),\n ...(collector !== undefined ? { hydrationDiagnostics: collector.sink } : {}),\n });\n const handle = renderer.hydrate(compile(build()), container);\n\n return {\n container,\n serverHtml,\n handle,\n diagnostics: collector?.diagnostics ?? [],\n flush() {\n handle.flush();\n },\n unmount() {\n handle.unmount();\n if (container.parentNode !== null) container.parentNode.removeChild(container);\n },\n };\n}\n"],"mappings":"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","names":["resetIdCounter","compile","BrowserDOMAdapter","createRenderer"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streetui/testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "StreetUI testing utilities — test renderer, query helpers, assertions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@streetui/core": "1.0.0",
|
|
29
|
+
"@streetui/graph": "1.0.0",
|
|
30
|
+
"@streetui/compiler": "1.0.0",
|
|
31
|
+
"@streetui/dsl": "1.0.0",
|
|
32
|
+
"@streetui/renderer": "1.0.0",
|
|
33
|
+
"@streetui/dom": "1.0.0",
|
|
34
|
+
"@streetui/runtime": "1.0.0",
|
|
35
|
+
"@streetui/scheduler": "1.0.0",
|
|
36
|
+
"@streetui/state": "1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "*",
|
|
40
|
+
"tsup": "*",
|
|
41
|
+
"vitest": "*",
|
|
42
|
+
"happy-dom": "*"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
54
|
+
}
|