@ultimat3/render 22.15.0 → 24.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.
@@ -1,91 +0,0 @@
1
- /**
2
- * The controller's test harness beside the fake DOM: a page with the links every test follows, a
3
- * tab whose `fetch` the test answers (every call recorded), an event dispatched the way a browser
4
- * does, and the router the test started — stopped after each by `stopRouter()`.
5
- */
6
- import { startNavigation } from './navigation';
7
- import {
8
- FakeDocument,
9
- type FakeElement,
10
- type FakeWindow,
11
- fakeWindow,
12
- h,
13
- htmlAnswer,
14
- routerHead,
15
- } from './navigation-dom-fixture';
16
-
17
- export type Handler = (init: RequestInit | undefined) => Response | Promise<Response>;
18
-
19
- export const page = (
20
- title: string,
21
- meta: { scope?: string; build?: string } = {},
22
- body: FakeElement[] = [],
23
- ) =>
24
- new FakeDocument(routerHead(title, [], meta), [
25
- h('main', {}, [
26
- h('h1', {}, title),
27
- h('a', { id: 'to-b', href: '/b' }, 'b'),
28
- h('a', { id: 'to-c', href: '/c' }, 'c'),
29
- h('a', { id: 'to-slow', href: '/slow' }, 'slow'),
30
- h('a', { id: 'plain', href: '/b', 'data-x-no-prefetch': '' }, 'plain'),
31
- ...body,
32
- ]),
33
- h('script', {}, `hydrate(${title})`),
34
- ]);
35
-
36
- /** The router the current test started; `stopRouter()` in an `afterEach` lets it go. */
37
- let router: ReturnType<typeof startNavigation>;
38
- export const currentRouter = (): ReturnType<typeof startNavigation> => router;
39
- export function stopRouter(): void {
40
- router?.stop();
41
- router = undefined;
42
- }
43
-
44
- /** A tab on `/a` whose `fetch` answers from `routes`; `calls` records `METHOD path purpose`. */
45
- export function tab(routes: Record<string, Handler>, doc = page('A')) {
46
- const calls: string[] = [];
47
- const fetch = (url: string, init?: RequestInit): Promise<Response> => {
48
- const u = new URL(url);
49
- const headers = (init?.headers ?? {}) as Record<string, string>;
50
- calls.push(
51
- `${init?.method ?? 'GET'} ${u.pathname}${u.search} ${headers['x-ultimate-navigation']}`,
52
- );
53
- const handler = routes[u.pathname];
54
- if (handler === undefined) return Promise.reject(new TypeError('network'));
55
- return Promise.resolve(handler(init));
56
- };
57
- const win = fakeWindow(doc, 'https://app.test/a', fetch);
58
- router = startNavigation(win as unknown as Window);
59
- return { win, doc, calls };
60
- }
61
-
62
- /** An event the way a browser dispatches it: cancelable, with its target and fields. */
63
- export function fire(
64
- on: EventTarget,
65
- type: string,
66
- target: unknown,
67
- fields: Record<string, unknown> = {},
68
- ): Event {
69
- const event = new Event(type, { cancelable: true, bubbles: true });
70
- Object.defineProperty(event, 'target', { value: target });
71
- for (const [key, value] of Object.entries({
72
- button: 0,
73
- metaKey: false,
74
- ctrlKey: false,
75
- shiftKey: false,
76
- altKey: false,
77
- ...fields,
78
- })) {
79
- Object.defineProperty(event, key, { value });
80
- }
81
- on.dispatchEvent(event);
82
- return event;
83
- }
84
-
85
- export const click = (win: FakeWindow, id: string, fields?: Record<string, unknown>) =>
86
- fire(win, 'click', win.document.getElementById(id), fields);
87
-
88
- export const answers = {
89
- b: () => htmlAnswer('page:B', page('B')),
90
- c: () => htmlAnswer('page:C', page('C')),
91
- };
@@ -1,424 +0,0 @@
1
- /**
2
- * Just enough DOM for the client router's unit tests, written for them the way `hydrate-replay`'s
3
- * fakes are: elements with attributes and children, the selectors the router uses (tag, `[a]`,
4
- * `[a="v"]`, `[a~="v"]`, `:not([src])`, comma lists), documents that adopt and swap bodies, a
5
- * window with history, location and timers, and a `DOMParser` that answers a fetched string with a
6
- * document the test built. What a browser DOES with a document is proven in Chrome
7
- * (`packages/cli/e2e/client-navigation-*.e2e.test.ts`); this is the branch coverage beside it.
8
- */
9
-
10
- type Attrs = Readonly<Record<string, string>>;
11
-
12
- /** One compound selector: a tag and attribute conditions, optionally excluding a `src`. */
13
- interface Simple {
14
- readonly tag: string;
15
- readonly conditions: readonly { name: string; op: string | undefined; value: string }[];
16
- readonly notSrc: boolean;
17
- }
18
-
19
- const parse = (selector: string): readonly Simple[] =>
20
- selector.split(',').map((part) => {
21
- const text = part.trim();
22
- const notSrc = text.includes(':not([src])');
23
- const clean = text.replace(':not([src])', '');
24
- const tag = /^[a-z0-9]*/i.exec(clean)?.[0] ?? '';
25
- const conditions = [...clean.matchAll(/\[([\w:-]+)(?:(~?=)"?([^"\]]*)"?)?\]/g)].map((m) => ({
26
- name: m[1] ?? '',
27
- op: m[2],
28
- value: m[3] ?? '',
29
- }));
30
- return { tag: tag.toUpperCase(), conditions, notSrc };
31
- });
32
-
33
- const matches = (el: FakeElement, selector: string): boolean =>
34
- parse(selector).some(
35
- (simple) =>
36
- (simple.tag === '' || simple.tag === el.tagName) &&
37
- !(simple.notSrc && el.hasAttribute('src')) &&
38
- simple.conditions.every(({ name, op, value }) => {
39
- const said = el.getAttribute(name);
40
- if (said === null) return false;
41
- if (op === undefined) return true;
42
- if (op === '~=') return said.split(/\s+/).includes(value);
43
- return said === value;
44
- }),
45
- );
46
-
47
- export class FakeElement {
48
- readonly tagName: string;
49
- readonly attrs = new Map<string, string>();
50
- children: FakeElement[] = [];
51
- parentElement: FakeElement | null = null;
52
- ownerDocument: FakeDocument;
53
- text = '';
54
- style: Record<string, string> = {};
55
- nonce = '';
56
- async = true;
57
- onload: (() => void) | null = null;
58
- onerror: (() => void) | null = null;
59
- __x?: Promise<unknown>;
60
- clicks = 0;
61
-
62
- constructor(
63
- doc: FakeDocument,
64
- tag: string,
65
- attrs: Attrs = {},
66
- content: readonly FakeElement[] | string = [],
67
- ) {
68
- this.ownerDocument = doc;
69
- this.tagName = tag.toUpperCase();
70
- for (const [name, value] of Object.entries(attrs)) this.attrs.set(name, value);
71
- if (typeof content === 'string') this.text = content;
72
- else for (const child of content) this.append(child);
73
- }
74
-
75
- get id(): string {
76
- return this.getAttribute('id') ?? '';
77
- }
78
- getAttribute(name: string): string | null {
79
- return this.attrs.get(name) ?? null;
80
- }
81
- setAttribute(name: string, value: string): void {
82
- this.attrs.set(name, String(value));
83
- }
84
- removeAttribute(name: string): void {
85
- this.attrs.delete(name);
86
- }
87
- hasAttribute(name: string): boolean {
88
- return this.attrs.has(name);
89
- }
90
- getAttributeNames(): string[] {
91
- return [...this.attrs.keys()];
92
- }
93
- get attributes(): { name: string; value: string }[] {
94
- return [...this.attrs].map(([name, value]) => ({ name, value }));
95
- }
96
- get textContent(): string {
97
- return this.text + this.children.map((child) => child.textContent).join('');
98
- }
99
- set textContent(value: string) {
100
- this.text = value;
101
- this.children = [];
102
- }
103
- get className(): string {
104
- return this.getAttribute('class') ?? '';
105
- }
106
- get outerHTML(): string {
107
- const attrs = [...this.attrs].map(([n, v]) => ` ${n}="${v}"`).join('');
108
- const tag = this.tagName.toLowerCase();
109
- return `<${tag}${attrs}>${this.text}${this.children.map((c) => c.outerHTML).join('')}</${tag}>`;
110
- }
111
- // The properties the router reads and writes on links, scripts and anchors.
112
- get href(): string {
113
- return new URL(this.getAttribute('href') ?? '', this.ownerDocument.baseURI).href;
114
- }
115
- set href(value: string) {
116
- this.setAttribute('href', value);
117
- }
118
- set rel(value: string) {
119
- this.setAttribute('rel', value);
120
- }
121
- get target(): string {
122
- return this.getAttribute('target') ?? '';
123
- }
124
- set download(value: string) {
125
- this.setAttribute('download', value);
126
- }
127
- get type(): string {
128
- return this.getAttribute('type') ?? '';
129
- }
130
-
131
- get connected(): boolean {
132
- let at: FakeElement | null = this;
133
- while (at.parentElement !== null) at = at.parentElement;
134
- return at === this.ownerDocument.documentElement;
135
- }
136
-
137
- /** A `src` script or a stylesheet link inserted into the live document "loads". */
138
- private loaded(): void {
139
- if (!this.connected) return;
140
- if (this.tagName === 'SCRIPT')
141
- this.ownerDocument.ran.push(this.getAttribute('src') ?? this.text);
142
- if (this.tagName === 'SCRIPT' || this.tagName === 'LINK') {
143
- const failed = (this.getAttribute('src') ?? this.getAttribute('href') ?? '').includes('fail');
144
- queueMicrotask(() => (failed ? this.onerror?.() : this.onload?.()));
145
- }
146
- }
147
-
148
- append(...nodes: FakeElement[]): void {
149
- for (const node of nodes) {
150
- node.remove();
151
- node.parentElement = this;
152
- node.adopt(this.ownerDocument);
153
- this.children.push(node);
154
- node.loaded();
155
- }
156
- }
157
- remove(): void {
158
- const parent = this.parentElement;
159
- if (parent === null) return;
160
- parent.children = parent.children.filter((child) => child !== this);
161
- this.parentElement = null;
162
- }
163
- replaceWith(node: FakeElement): void {
164
- const parent = this.parentElement;
165
- if (parent === null) return;
166
- node.remove();
167
- const index = parent.children.indexOf(this);
168
- parent.children.splice(index, 1, node);
169
- node.parentElement = parent;
170
- this.parentElement = null;
171
- node.adopt(parent.ownerDocument);
172
- node.loaded();
173
- }
174
- replaceChild(next: FakeElement, old: FakeElement): void {
175
- old.replaceWith(next);
176
- }
177
- adopt(doc: FakeDocument): void {
178
- this.ownerDocument = doc;
179
- for (const child of this.children) child.adopt(doc);
180
- }
181
- contains(node: FakeElement): boolean {
182
- for (let at: FakeElement | null = node; at !== null; at = at.parentElement) {
183
- if (at === this) return true;
184
- }
185
- return false;
186
- }
187
- private descendants(): FakeElement[] {
188
- return this.children.flatMap((child) => [child, ...child.descendants()]);
189
- }
190
- querySelectorAll(selector: string): FakeElement[] {
191
- return this.descendants().filter((el) => matches(el, selector));
192
- }
193
- querySelector(selector: string): FakeElement | null {
194
- return this.querySelectorAll(selector)[0] ?? null;
195
- }
196
- closest(selector: string): FakeElement | null {
197
- for (let at: FakeElement | null = this; at !== null; at = at.parentElement) {
198
- if (matches(at, selector)) return at;
199
- }
200
- return null;
201
- }
202
- focus(): void {
203
- this.ownerDocument.activeElement = this;
204
- }
205
- click(): void {
206
- this.clicks += 1;
207
- }
208
- }
209
-
210
- /** A form the router's `instanceof HTMLFormElement` accepts once installed as that global. */
211
- export class FakeForm extends FakeElement {
212
- fields: [string, string | File][] = [];
213
- }
214
-
215
- export class FakeDocument extends EventTarget {
216
- readonly documentElement: FakeElement;
217
- baseURI = 'https://app.test/';
218
- activeElement: FakeElement | null = null;
219
- /** Every script that "ran": its `src`, or its text for an inline one. */
220
- readonly ran: string[] = [];
221
- startViewTransition?: (update: () => void) => {
222
- updateCallbackDone: Promise<void>;
223
- ready: Promise<void>;
224
- finished: Promise<void>;
225
- };
226
-
227
- constructor(head: readonly FakeElement[] = [], body: readonly FakeElement[] = [], lang = 'en') {
228
- super();
229
- this.documentElement = new FakeElement(this, 'html', { lang });
230
- this.documentElement.append(new FakeElement(this, 'head'), new FakeElement(this, 'body'));
231
- this.head.append(...head.map((el) => this.take(el)));
232
- this.body.append(...body.map((el) => this.take(el)));
233
- }
234
- private take(el: FakeElement): FakeElement {
235
- el.adopt(this);
236
- return el;
237
- }
238
- get head(): FakeElement {
239
- return this.documentElement.children.find((c) => c.tagName === 'HEAD') as FakeElement;
240
- }
241
- get body(): FakeElement {
242
- return this.documentElement.children.find((c) => c.tagName === 'BODY') as FakeElement;
243
- }
244
- get title(): string {
245
- return this.head.querySelector('title')?.textContent ?? '';
246
- }
247
- set title(value: string) {
248
- const el = this.head.querySelector('title');
249
- if (el === null) this.head.append(new FakeElement(this, 'title', {}, value));
250
- else el.textContent = value;
251
- }
252
- createElement(tag: string): FakeElement {
253
- return new FakeElement(this, tag);
254
- }
255
- adoptNode(node: FakeElement): FakeElement {
256
- node.remove();
257
- node.adopt(this);
258
- return node;
259
- }
260
- /** What a test says is under the pointer, for a click the browser aimed at `<html>`. */
261
- underPointer: FakeElement | null = null;
262
- elementFromPoint(): FakeElement | null {
263
- return this.underPointer;
264
- }
265
- getElementById(id: string): FakeElement | null {
266
- return this.documentElement.querySelectorAll('[id]').find((el) => el.id === id) ?? null;
267
- }
268
- querySelector(selector: string): FakeElement | null {
269
- return this.documentElement.querySelector(selector);
270
- }
271
- querySelectorAll(selector: string): FakeElement[] {
272
- return this.documentElement.querySelectorAll(selector);
273
- }
274
- }
275
-
276
- /** `h('a', { href: '/b' }, 'b')` — builds an element owned by a scratch document until adopted. */
277
- const scratch = new FakeDocument();
278
- export const h = (
279
- tag: string,
280
- attrs: Attrs = {},
281
- content: readonly FakeElement[] | string = [],
282
- ): FakeElement =>
283
- tag === 'form'
284
- ? new FakeForm(scratch, tag, attrs, content)
285
- : new FakeElement(scratch, tag, attrs, content);
286
-
287
- /** The head an opted-in document carries, plus whatever the page adds. */
288
- export const routerHead = (
289
- title: string,
290
- extra: readonly FakeElement[] = [],
291
- meta: { surface?: string; build?: string; scope?: string } = {},
292
- ): FakeElement[] => [
293
- h('title', {}, title),
294
- h('meta', { name: 'ultimate-navigation', content: meta.surface ?? 'web:app' }),
295
- h('meta', { name: 'x-ultimate-build', content: meta.build ?? 'b1' }),
296
- ...(meta.scope === undefined ? [] : [h('meta', { name: 'ultimate-scope', content: meta.scope })]),
297
- ...extra,
298
- ];
299
-
300
- interface HistoryEntry {
301
- state: unknown;
302
- url: string;
303
- }
304
-
305
- /** A window: location, history (with a back/forward that fires `popstate`), timers, `fetch`. */
306
- export class FakeWindow extends EventTarget {
307
- document: FakeDocument;
308
- readonly assigned: string[] = [];
309
- readonly entries: HistoryEntry[];
310
- index = 0;
311
- scrollX = 0;
312
- scrollY = 0;
313
- reducedMotion = false;
314
- navigator: { connection?: { saveData?: boolean; effectiveType?: string } } = {};
315
- readonly DOMParser = true;
316
- readonly BroadcastChannel = true;
317
- fetch: (url: string, init?: RequestInit) => Promise<Response>;
318
- __xNavigation?: unknown;
319
-
320
- constructor(doc: FakeDocument, url: string, fetch: FakeWindow['fetch']) {
321
- super();
322
- this.document = doc;
323
- this.entries = [{ state: null, url }];
324
- this.fetch = fetch;
325
- }
326
-
327
- readonly location = {
328
- get href(): string {
329
- return self.entries[self.index]?.url ?? '';
330
- },
331
- assign: (url: string): void => {
332
- this.assigned.push(url);
333
- },
334
- };
335
-
336
- readonly history = {
337
- scrollRestoration: 'auto',
338
- get state(): unknown {
339
- return self.entries[self.index]?.state ?? null;
340
- },
341
- pushState: (state: unknown, _title: string, url?: string): void => {
342
- this.entries.splice(this.index + 1);
343
- this.entries.push({ state, url: url ?? this.location.href });
344
- this.index += 1;
345
- },
346
- replaceState: (state: unknown, _title: string, url?: string): void => {
347
- this.entries[this.index] = { state, url: url ?? this.location.href };
348
- },
349
- };
350
-
351
- /** Back or forward, as a browser does it: the entry moves, then `popstate` fires. */
352
- go(delta: number): void {
353
- this.index += delta;
354
- this.dispatchEvent(Object.assign(new Event('popstate'), { state: this.history.state }));
355
- }
356
-
357
- scrollTo(options: { left: number; top: number }): void {
358
- this.scrollX = options.left;
359
- this.scrollY = options.top;
360
- }
361
- matchMedia(): { matches: boolean } {
362
- return { matches: this.reducedMotion };
363
- }
364
- setTimeout(fn: () => void, ms?: number): number {
365
- return globalThis.setTimeout(fn, ms) as unknown as number;
366
- }
367
- clearTimeout(id: number | undefined): void {
368
- globalThis.clearTimeout(id);
369
- }
370
- }
371
-
372
- // `location` and `history` are object literals whose getters need the window; `self` is set per
373
- // instance by `fakeWindow`, one window per test.
374
- let self: FakeWindow;
375
-
376
- /** The window a test drives; installs the three globals the router reaches for by name. */
377
- export function fakeWindow(doc: FakeDocument, url: string, fetch: FakeWindow['fetch']): FakeWindow {
378
- self = new FakeWindow(doc, url, fetch);
379
- const globals = globalThis as unknown as Record<string, unknown>;
380
- globals['DOMParser'] = FakeParser;
381
- globals['HTMLFormElement'] = FakeForm;
382
- globals['FormData'] = FakeFormData;
383
- globals['HTMLElement'] = FakeElement;
384
- return self;
385
- }
386
-
387
- /** Documents a `DOMParser` answers, keyed by the HTML string a fake `fetch` returned. */
388
- export const parsed = new Map<string, FakeDocument>();
389
-
390
- class FakeParser {
391
- parseFromString(html: string): FakeDocument {
392
- const doc = parsed.get(html);
393
- if (doc === undefined) throw new TypeError(`no fake document registered for ${html}`);
394
- return doc;
395
- }
396
- }
397
-
398
- const RealFormData = globalThis.FormData;
399
- class FakeFormData extends RealFormData {
400
- constructor(form?: FakeForm, submitter?: FakeElement | null) {
401
- super();
402
- for (const [name, value] of form?.fields ?? []) this.append(name, value);
403
- const name = submitter?.getAttribute('name');
404
- if (name !== undefined && name !== null)
405
- this.append(name, submitter?.getAttribute('value') ?? '');
406
- }
407
- }
408
-
409
- /** A registered page: `answer('/b', doc)` is what the fake fetch returns for `/b`. */
410
- export function htmlAnswer(key: string, doc: FakeDocument, init: ResponseInit = {}): Response {
411
- parsed.set(key, doc);
412
- return new Response(key, {
413
- ...init,
414
- headers: {
415
- 'content-type': 'text/html; charset=utf-8',
416
- ...(init.headers as Record<string, string>),
417
- },
418
- });
419
- }
420
-
421
- /** Lets queued microtasks and a few macrotasks run — a swap awaits loads, transitions, scripts. */
422
- export const settle = async (ms = 5): Promise<void> => {
423
- for (let i = 0; i < 5; i += 1) await new Promise((resolve) => setTimeout(resolve, ms));
424
- };