Files
Webklar-Kundenbereich/test/support/fakeDom.js
T
ClaudeandClaude Sonnet 5.5 5f99f08a39 TP2 Abschlussprüfung: Fragebogen und Bestellseite (veralteter Tab, Abo-Hinweis, Zurück aus Stripe)
- PUT /fragebogen: ein Tab mit veralteter Branche bekommt 409 EINSTELLUNG_VERALTET (die Branche wechselt nur die Vorlage), bestaetigt wird mit
  dem gespeicherten Stand vereinigt; die ungeprüften Vorlagenpreise können nicht mehr als geprüft gelten
- GET /einstellungen liefert in off aboHinweis (aboMeldung, auch bei nicht lesbarem Abo); die Hinweiskarte zeigt ihn immer und hat „Neu laden“
- Bestellseite und Statuskarte laden bei pageshow (persisted) neu, wenn der Browser sie nach der Zahlungsseite aus dem bfcache holt
- renderBestellen zeichnet „Ändern“ und „Zum Schritt“ nur mit zumSchritt; nach einem 409 bekommt die Meldung den Fokus
- Zusammenfassung nennt „Kleinunternehmer: ja/nein“

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:35:42 +00:00

545 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Minimal-DOM und Fake-fetch fuer Tests des Portal-Clients (public/termine*.js, public/funktion-bestellen.js) ohne Browser. Nicht Teil von npm test.
//
// const fx = installFakeDom({ antworten: [{ status: 400, body: { fehler: {...} } }] })
// try { ...Client-Code bauen, fx.klick(knopf), fx.fetchLog pruefen... } finally { fx.restore() }
//
// installFakeDom stellt `document`, `window` (location mit href, pathname, search, hash und reload() - jeder Aufruf steht in fx.reloads -, history.replaceState und
// addEventListener/removeEventListener/dispatchEvent fuer Ereignisse des Fensters wie 'pageshow'), `CSS.escape` und `fetch` auf globalThis, restore() stellt den
// alten Zustand wieder her. `h` aus public/termine-dom.js liest `document` bei jedem Aufruf: der Client braucht keine Einhaengepunkte.
// Wurzeln, die `isConnected` brauchen (neuerSpeicher.nochHier), haengt der Test selbst an `document.body`.
//
// Elemente: createElement, setAttribute/getAttribute/hasAttribute/removeAttribute, append/prepend/appendChild/insertBefore/replaceChildren/remove,
// children/childNodes/parentNode/parentElement/firstChild/lastChild/firstElementChild/lastElementChild, className/classList/id/dataset/hidden/
// disabled/tabIndex/type/name, value/checked/selected (Eigenschaft ueberdeckt das Attribut wie im Browser; select.value ist die gewaehlte
// Option, ohne Wahl die erste), textContent, isConnected, closest/matches/querySelector/querySelectorAll, focus (document.activeElement)/blur/
// select/scrollIntoView, addEventListener/dispatchEvent (Ereignisse steigen zu den Eltern auf, preventDefault/stopPropagation).
// Selektoren: Tag, *, #id, .klasse, [attr], [attr="wert"], :not(…), :disabled, :enabled, :checked, Komma, Leerzeichen und >.
//
// Der Fake-fetch schreibt jeden Aufruf als { method, url, body } (body = geparstes JSON oder undefined) in fetchLog und antwortet mit dem naechsten
// Eintrag aus `antworten` ({ status, body } oder eine Funktion (aufruf) => { status, body }); ohne Eintrag 200 mit {}.
// klick, setze, sende und ruhe liefern ein Promise: die Wirkung der Ereignisse (auch von async-Handlern) ist erst danach da, also `await fx.klick(…)`.
// klick wirkt wie ein Klick im Browser (Haken und Radio schalten, „submit“-Knopf sendet das Formular, gesperrte Elemente tun nichts); ein Klick auf ein
// <label> schaltet seinen Haken NICHT: der Test klickt den Haken selbst. sende(el, typ, init) schickt ein beliebiges Ereignis (z. B. 'keydown', { key }).
import { setImmediate as naechsterTakt } from 'node:timers/promises'
/* --------------------------------------------------------------- CSS.escape --- */
/** CSS.escape nach der Spezifikation (die Client-Dateien bauen damit Attribut-Selektoren aus Feldnamen wie 'leistungen.2.dauer'). */
export function cssEscape(wert) {
const s = String(wert)
let aus = ''
for (let i = 0; i < s.length; i += 1) {
const c = s[i]
const code = s.charCodeAt(i)
if (code === 0) aus += '\uFFFD'
else if ((code >= 1 && code <= 31) || code === 127 || (i === 0 && c >= '0' && c <= '9') || (i === 1 && c >= '0' && c <= '9' && s[0] === '-')) aus += `\\${code.toString(16)} `
else if (i === 0 && c === '-' && s.length === 1) aus += `\\${c}`
else if (code >= 128 || c === '-' || c === '_' || /[0-9A-Za-z]/.test(c)) aus += c
else aus += `\\${c}`
}
return aus
}
const entescape = (text) => text.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g, (_, hex, zeichen) => (hex ? String.fromCodePoint(parseInt(hex, 16)) : zeichen))
/* ---------------------------------------------------------------- Selektoren --- */
const STEUERUNGEN = new Set(['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET', 'OPTION'])
/** :disabled – eigenes Attribut oder ein gesperrtes fieldset darueber (wie im Browser). */
function istGesperrt(el) {
if (!STEUERUNGEN.has(el.tagName)) return false
if (el.hasAttribute('disabled')) return true
for (let p = el.parentNode; p && p.nodeType === 1; p = p.parentNode) if (p.tagName === 'FIELDSET' && p.hasAttribute('disabled')) return true
return false
}
const TEIL_QUELLE = /(\*|[A-Za-z][\w-]*)|#((?:\\.|[\w-])+)|\.((?:\\.|[\w-])+)|\[\s*([\w:-]+)\s*(?:(=)\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\]\s]*)))?\s*\]|:not\(([^)]*)\)|:([a-z-]+)/
/** Ein einfacher Selektor (ohne Leerzeichen/Komma) als Pruefung el => boolean. */
function einfach(text) {
const tests = []
const teil = new RegExp(TEIL_QUELLE.source, 'y') // je Aufruf eigener Zustand: :not(…) ruft einfach() verschachtelt auf
while (teil.lastIndex < text.length) {
const m = teil.exec(text)
if (!m) throw new Error(`fakeDom: Selektor nicht unterstuetzt: ${text}`)
const [, tag, id, klasse, attr, op, doppelt, einzeln, blank, nicht, pseudo] = m
if (tag) tests.push(tag === '*' ? () => true : (el) => el.tagName === tag.toUpperCase())
else if (id) tests.push((el) => el.id === entescape(id))
else if (klasse) tests.push((el) => el.classList.contains(entescape(klasse)))
else if (attr) {
const soll = op ? entescape(doppelt ?? einzeln ?? blank ?? '') : null
tests.push((el) => (op ? el.getAttribute(attr) === soll : el.hasAttribute(attr)))
} else if (nicht !== undefined) {
const liste = parse(nicht)
tests.push((el) => !liste.some((komplex) => passt(el, komplex)))
} else if (pseudo === 'disabled') tests.push((el) => istGesperrt(el))
else if (pseudo === 'enabled') tests.push((el) => STEUERUNGEN.has(el.tagName) && !istGesperrt(el))
else if (pseudo === 'checked') tests.push((el) => (el.tagName === 'OPTION' ? el.selected : el.tagName === 'INPUT' && el.checked))
else throw new Error(`fakeDom: Pseudo-Klasse nicht unterstuetzt: :${pseudo}`)
}
return (el) => tests.every((test) => test(el))
}
/** Selektorliste ⇒ Array von Ketten [{ komb, test }] (komb = Verbindung zum Vorgaenger: ' ' Nachfahre, '>' Kind). */
function parse(quelle) {
const liste = []
let kette = []
let stueck = ''
let komb = ' '
let tiefe = 0
let quote = null
const schliesse = () => {
if (!stueck) return
kette.push({ komb, test: einfach(stueck) })
stueck = ''
komb = ' '
}
for (let i = 0; i < quelle.length; i += 1) {
const zeichen = quelle[i]
if (zeichen === '\\') { stueck += zeichen + (quelle[i + 1] ?? ''); i += 1; continue }
if (quote) { stueck += zeichen; if (zeichen === quote) quote = null; continue }
if (zeichen === '"' || zeichen === "'") { quote = zeichen; stueck += zeichen; continue }
if (zeichen === '[' || zeichen === '(') tiefe += 1
if (zeichen === ']' || zeichen === ')') tiefe -= 1
if (tiefe === 0 && zeichen === ',') { schliesse(); if (kette.length) liste.push(kette); kette = []; continue }
if (tiefe === 0 && /\s/.test(zeichen)) { schliesse(); continue }
if (tiefe === 0 && zeichen === '>') { schliesse(); komb = '>'; continue }
stueck += zeichen
}
schliesse()
if (kette.length) liste.push(kette)
if (!liste.length) throw new Error(`fakeDom: leerer Selektor: "${quelle}"`)
return liste
}
function passt(el, kette, i = kette.length - 1) {
if (!kette[i].test(el)) return false
if (i === 0) return true
if (kette[i].komb === '>') return el.parentNode?.nodeType === 1 && passt(el.parentNode, kette, i - 1)
for (let p = el.parentNode; p && p.nodeType === 1; p = p.parentNode) if (passt(p, kette, i - 1)) return true
return false
}
const trifft = (el, selektor) => parse(selektor).some((kette) => passt(el, kette))
/* -------------------------------------------------------------------- Knoten --- */
class FakeText {
constructor(text) {
this.nodeType = 3
this.data = String(text)
this.parentNode = null
}
get textContent() { return this.data }
set textContent(wert) { this.data = String(wert) }
}
const kebab = (name) => `data-${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`
const alsKnoten = (doc, wert) => (typeof wert === 'object' && wert !== null ? wert : new FakeText(wert))
class FakeElement {
constructor(doc, tag) {
this.ownerDocument = doc
this.nodeType = 1
this.tagName = String(tag).toUpperCase()
this.parentNode = null
this.childNodes = []
this._attrs = new Map()
this._listeners = new Map()
this._value = undefined
this._checked = undefined
this._selected = undefined
const el = this
this.dataset = new Proxy({}, {
get: (_, name) => (typeof name === 'string' ? el._attrs.get(kebab(name)) : undefined),
set: (_, name, wert) => { el.setAttribute(kebab(name), wert); return true },
has: (_, name) => el._attrs.has(kebab(name)),
deleteProperty: (_, name) => { el._attrs.delete(kebab(name)); return true },
})
const liste = () => (el._attrs.get('class') ?? '').split(/\s+/).filter(Boolean)
this.classList = {
contains: (name) => liste().includes(name),
add: (...namen) => el.setAttribute('class', [...new Set([...liste(), ...namen])].join(' ')),
remove: (...namen) => el.setAttribute('class', liste().filter((n) => !namen.includes(n)).join(' ')),
toggle: (name, an) => {
const soll = an ?? !liste().includes(name)
if (soll) this.classList.add(name)
else this.classList.remove(name)
return soll
},
}
}
/* Attribute */
setAttribute(name, wert) { this._attrs.set(name, String(wert)) }
getAttribute(name) { return this._attrs.has(name) ? this._attrs.get(name) : null }
hasAttribute(name) { return this._attrs.has(name) }
removeAttribute(name) { this._attrs.delete(name) }
get id() { return this._attrs.get('id') ?? '' }
set id(wert) { this.setAttribute('id', wert) }
get className() { return this._attrs.get('class') ?? '' }
set className(wert) { this.setAttribute('class', wert) }
get hidden() { return this._attrs.has('hidden') }
set hidden(an) { if (an) this.setAttribute('hidden', ''); else this.removeAttribute('hidden') }
get disabled() { return this._attrs.has('disabled') }
set disabled(an) { if (an) this.setAttribute('disabled', ''); else this.removeAttribute('disabled') }
get readOnly() { return this._attrs.has('readonly') }
set readOnly(an) { if (an) this.setAttribute('readonly', ''); else this.removeAttribute('readonly') }
get tabIndex() { return this._attrs.has('tabindex') ? Number(this._attrs.get('tabindex')) : -1 }
set tabIndex(wert) { this.setAttribute('tabindex', wert) }
get type() { return this._attrs.get('type') ?? (this.tagName === 'BUTTON' ? 'submit' : 'text') }
set type(wert) { this.setAttribute('type', wert) }
get name() { return this._attrs.get('name') ?? '' }
set name(wert) { this.setAttribute('name', wert) }
/* value, checked, selected: die Eigenschaft ueberdeckt das Attribut, sobald sie gesetzt ist */
get value() {
if (this.tagName === 'SELECT') {
const optionen = this.querySelectorAll('option')
return (optionen.find((o) => o.selected) ?? optionen.find((o) => !o.disabled))?.value ?? ''
}
if (this.tagName === 'OPTION') return this._attrs.get('value') ?? this.textContent
if (this.tagName === 'TEXTAREA') return this._value ?? this.textContent
return this._value ?? this._attrs.get('value') ?? (this.type === 'checkbox' || this.type === 'radio' ? 'on' : '')
}
set value(wert) {
if (this.tagName === 'SELECT') {
for (const o of this.querySelectorAll('option')) o._selected = o.value === String(wert)
} else if (this.tagName === 'OPTION') this.setAttribute('value', wert)
else this._value = String(wert)
}
get checked() { return this._checked ?? this._attrs.has('checked') }
set checked(an) {
this._checked = Boolean(an)
if (an && this.type === 'radio' && this.name) {
let wurzel = this
while (wurzel.parentNode) wurzel = wurzel.parentNode
for (const r of wurzel.querySelectorAll('input')) if (r !== this && r.type === 'radio' && r.name === this.name) r._checked = false
}
}
get selected() { return this._selected ?? this._attrs.has('selected') }
set selected(an) {
this._selected = Boolean(an)
if (an && this.parentNode?.tagName === 'SELECT') for (const o of this.parentNode.querySelectorAll('option')) if (o !== this) o._selected = false
}
/* Baum */
get children() { return this.childNodes.filter((n) => n.nodeType === 1) }
get parentElement() { return this.parentNode?.nodeType === 1 ? this.parentNode : null }
get firstChild() { return this.childNodes[0] ?? null }
get lastChild() { return this.childNodes.at(-1) ?? null }
get firstElementChild() { return this.children[0] ?? null }
get lastElementChild() { return this.children.at(-1) ?? null }
get isConnected() {
let wurzel = this
while (wurzel.parentNode) wurzel = wurzel.parentNode
return wurzel === this.ownerDocument.documentElement
}
get textContent() { return this.childNodes.map((n) => n.textContent).join('') }
set textContent(wert) { this.replaceChildren(...(wert === '' || wert === null || wert === undefined ? [] : [String(wert)])) }
_loese(knoten) {
if (knoten.parentNode) knoten.parentNode.removeChild(knoten)
return knoten
}
append(...knoten) {
for (const wert of knoten) {
const k = this._loese(alsKnoten(this.ownerDocument, wert))
k.parentNode = this
this.childNodes.push(k)
}
}
prepend(...knoten) {
const neu = knoten.map((wert) => this._loese(alsKnoten(this.ownerDocument, wert)))
for (const k of neu) k.parentNode = this
this.childNodes.unshift(...neu)
}
appendChild(knoten) { this.append(knoten); return knoten }
insertBefore(knoten, vor) {
if (!vor) return this.appendChild(knoten)
this._loese(knoten)
knoten.parentNode = this
this.childNodes.splice(this.childNodes.indexOf(vor), 0, knoten)
return knoten
}
removeChild(knoten) {
const i = this.childNodes.indexOf(knoten)
if (i < 0) throw new Error('fakeDom: removeChild: kein Kind dieses Elements')
this.childNodes.splice(i, 1)
knoten.parentNode = null
return knoten
}
replaceChildren(...knoten) {
for (const k of this.childNodes) k.parentNode = null
this.childNodes = []
this.append(...knoten)
}
remove() { this.parentNode?.removeChild(this) }
contains(knoten) {
for (let k = knoten; k; k = k.parentNode) if (k === this) return true
return false
}
/* Suche */
matches(selektor) { return trifft(this, selektor) }
closest(selektor) {
for (let el = this; el && el.nodeType === 1; el = el.parentNode) if (trifft(el, selektor)) return el
return null
}
querySelectorAll(selektor) {
const liste = parse(selektor)
const treffer = []
const durchlauf = (el) => {
for (const kind of el.children) {
if (liste.some((kette) => passt(kind, kette))) treffer.push(kind)
durchlauf(kind)
}
}
durchlauf(this)
return treffer
}
querySelector(selektor) { return this.querySelectorAll(selektor)[0] ?? null }
/* Fokus */
focus() {
if (!this.isConnected || istGesperrt(this)) return
const doc = this.ownerDocument
const davor = doc._aktiv
doc._aktiv = this
if (davor !== this) feuere(this, neuesEreignis('focus', { bubbles: false }))
}
blur() { if (this.ownerDocument._aktiv === this) this.ownerDocument._aktiv = null }
select() {}
scrollIntoView() {}
click() { return klickEl(this) }
/* Ereignisse */
addEventListener(typ, fn) {
if (!this._listeners.has(typ)) this._listeners.set(typ, [])
this._listeners.get(typ).push(fn)
}
removeEventListener(typ, fn) {
const liste = this._listeners.get(typ)
if (liste?.includes(fn)) liste.splice(liste.indexOf(fn), 1)
}
dispatchEvent(ev) {
feuere(this, ev)
return !ev.defaultPrevented
}
}
/* ----------------------------------------------------------------- Ereignisse --- */
function neuesEreignis(typ, init = {}) {
return {
type: typ,
target: null,
currentTarget: null,
bubbles: true,
defaultPrevented: false,
stopped: false,
ergebnisse: [],
preventDefault() { this.defaultPrevented = true },
stopPropagation() { this.stopped = true },
...init,
}
}
/** Ereignis an el senden und zu den Eltern aufsteigen lassen; Rueckgaben der Handler (async-Handler: Promises) landen in ev.ergebnisse. */
function feuere(el, ev) {
ev.target = el
for (let knoten = el; knoten && knoten.nodeType === 1; knoten = knoten.parentNode) {
ev.currentTarget = knoten
for (const fn of [...(knoten._listeners.get(ev.type) ?? [])]) ev.ergebnisse.push(fn.call(knoten, ev))
if (ev.stopped || !ev.bubbles) break
}
}
/** Alle Handler eines Ereignisses abwarten und danach einen Takt, in dem Microtask-Ketten (fetch, await) durchlaufen. */
async function abwarten(ergebnisse) {
await Promise.all(ergebnisse)
await naechsterTakt()
}
/** Klick wie im Browser: Haken setzen/umschalten, click, input/change, bei „submit“-Knopf in einem Formular submit. */
function klickEl(el) {
if (istGesperrt(el)) return abwarten([])
const ergebnisse = []
const art = el.tagName === 'INPUT' ? el.type : null
if (art === 'checkbox') el.checked = !el.checked
else if (art === 'radio') el.checked = true
const klick = neuesEreignis('click')
feuere(el, klick)
ergebnisse.push(...klick.ergebnisse)
if (!klick.defaultPrevented) {
if (art === 'checkbox' || art === 'radio') {
for (const typ of ['input', 'change']) {
const ev = neuesEreignis(typ)
feuere(el, ev)
ergebnisse.push(...ev.ergebnisse)
}
} else if ((el.tagName === 'BUTTON' && el.type === 'submit') || (art === 'submit')) {
const form = el.closest('form')
if (form) {
const ev = neuesEreignis('submit', { submitter: el })
feuere(form, ev)
ergebnisse.push(...ev.ergebnisse)
}
}
}
return abwarten(ergebnisse)
}
/** Wert eingeben wie ein Nutzer: Text/Auswahl als String, Haken/Radio als Boolean; danach input und change. */
function setzeEl(el, wert) {
if (istGesperrt(el)) return abwarten([])
if (el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')) el.checked = Boolean(wert)
else el.value = wert
const ergebnisse = []
for (const typ of ['input', 'change']) {
const ev = neuesEreignis(typ)
feuere(el, ev)
ergebnisse.push(...ev.ergebnisse)
}
return abwarten(ergebnisse)
}
/* -------------------------------------------------------------------- Einbau --- */
function neuesDokument() {
const doc = { nodeType: 9, _aktiv: null }
doc.createElement = (tag) => new FakeElement(doc, tag)
doc.createTextNode = (text) => new FakeText(text)
doc.documentElement = doc.createElement('html')
doc.head = doc.createElement('head')
doc.body = doc.createElement('body')
doc.documentElement.append(doc.head, doc.body)
Object.defineProperty(doc, 'activeElement', { get: () => (doc._aktiv?.isConnected ? doc._aktiv : doc.body) })
doc.querySelector = (selektor) => (trifft(doc.documentElement, selektor) ? doc.documentElement : doc.documentElement.querySelector(selektor))
doc.querySelectorAll = (selektor) => doc.documentElement.querySelectorAll(selektor)
doc.getElementById = (id) => doc.documentElement.querySelector(`[id="${id}"]`)
return doc
}
const GLOBALE = ['document', 'window', 'CSS', 'fetch']
/**
* Fake-DOM und Fake-fetch auf globalThis stellen.
* => { document, window, fetchLog, reloads, antworten, finde, alle, text, klick, setze, sende, ruhe, restore }
* finde(baum, vergleich) / alle(baum, vergleich): erstes / alle Elemente im Baum (baum selbst eingeschlossen, Dokumentreihenfolge), fuer die
* vergleich(el) wahr ist (Funktion) oder die zum Selektor passen (String).
* text(baum): Text des Baums (textContent). klick(el), setze(el, wert), ruhe(): siehe Kopf der Datei.
*/
export function installFakeDom({ antworten = [] } = {}) {
const vorher = Object.fromEntries(GLOBALE.map((name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)]))
const document = neuesDokument()
const reloads = []
const location = { href: 'http://localhost/', pathname: '/dashboard.html', search: '', hash: '', reload: () => { reloads.push(location.href) } }
const fensterEreignisse = new Map()
const window = {
document,
location,
history: { replaceState: (_zustand, _titel, url) => { location.hash = String(url ?? '').includes('#') ? `#${String(url).split('#')[1]}` : '' } },
addEventListener(typ, fn) {
if (!fensterEreignisse.has(typ)) fensterEreignisse.set(typ, [])
fensterEreignisse.get(typ).push(fn)
},
removeEventListener(typ, fn) {
const liste = fensterEreignisse.get(typ)
if (liste?.includes(fn)) liste.splice(liste.indexOf(fn), 1)
},
/** Ereignis an die Handler des Fensters (z. B. { type: 'pageshow', persisted: true }); Rueckgaben werden nicht abgewartet. */
dispatchEvent(ev) {
for (const fn of [...(fensterEreignisse.get(ev.type) ?? [])]) fn(ev)
return true
},
/** Zahl der Handler eines Typs (fuer Tests, die Aufraeumen pruefen). */
anzahlHandler: (typ) => (fensterEreignisse.get(typ) ?? []).length,
}
const fetchLog = []
const warteschlange = [...antworten]
const fakeFetch = async (url, optionen = {}) => {
const aufruf = { method: optionen.method || 'GET', url: String(url), body: optionen.body ? JSON.parse(optionen.body) : undefined }
fetchLog.push(aufruf)
const eintrag = warteschlange.shift()
const { status = 200, body = {} } = (typeof eintrag === 'function' ? eintrag(aufruf) : eintrag) || {}
return { ok: status >= 200 && status < 300, status, json: async () => structuredClone(body) }
}
const neu = { document, window, CSS: { escape: cssEscape }, fetch: fakeFetch }
for (const [name, wert] of Object.entries(neu)) Object.defineProperty(globalThis, name, { value: wert, writable: true, configurable: true, enumerable: true })
const knoten = (baum) => [baum, ...(baum?.querySelectorAll?.('*') ?? [])].filter((n) => n?.nodeType === 1)
const pruefung = (vergleich) => (typeof vergleich === 'string' ? (el) => trifft(el, vergleich) : vergleich)
return {
document,
window,
fetchLog,
reloads,
antworten: warteschlange,
finde: (baum, vergleich) => knoten(baum).find(pruefung(vergleich)) ?? null,
alle: (baum, vergleich) => knoten(baum).filter(pruefung(vergleich)),
text: (baum) => baum?.textContent ?? '',
klick: klickEl,
setze: setzeEl,
sende: (el, typ, init) => {
const ev = neuesEreignis(typ, init)
feuere(el, ev)
return abwarten(ev.ergebnisse)
},
ruhe: () => abwarten([]),
restore() {
for (const name of GLOBALE) {
if (vorher[name]) Object.defineProperty(globalThis, name, vorher[name])
else delete globalThis[name]
}
},
}
}