149 lines
5.9 KiB
JavaScript
149 lines
5.9 KiB
JavaScript
// Testhilfen fuer die Funktionsmodule im Abo-Modell (P4 „Funktionsmodule“): Fake-DOM mit CustomEvent, Antworten nach Adresse, Attrappe fuer den
|
|
// Einschalt-Ablauf (zeigeEinschalten aus public/neu/funktion-einschalten.js), Wurzeln fuer die innerHTML-Module (News, Analyse).
|
|
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { installFakeDom } from './fakeDom.js'
|
|
|
|
/** Wörter, die im Abo-Modell auf keiner Oberfläche der Funktionen stehen dürfen (Spec: keine Preise für Funktionen). */
|
|
export const KEIN_PREIS = /€|einmalig|Preis|Zahlungspflichtig|Bestellseite|Behalten/
|
|
|
|
/** CustomEvent fuer fakeDom: type, detail, bubbles, target, ergebnisse, preventDefault, stopPropagation. */
|
|
class FakeCustomEvent {
|
|
constructor(type, init = {}) {
|
|
this.type = type
|
|
this.detail = init.detail ?? null
|
|
this.bubbles = init.bubbles === true
|
|
this.target = null
|
|
this.currentTarget = null
|
|
this.defaultPrevented = false
|
|
this.stopped = false
|
|
this.ergebnisse = []
|
|
}
|
|
|
|
preventDefault() { this.defaultPrevented = true }
|
|
|
|
stopPropagation() { this.stopped = true }
|
|
}
|
|
|
|
/** Testrumpf: Fake-DOM an, CustomEvent der Fake-Klasse, nach dem Test alles zurück. fn(fx). */
|
|
export const mitDom = (fn) => async () => {
|
|
const fx = installFakeDom()
|
|
const davor = Object.getOwnPropertyDescriptor(globalThis, 'CustomEvent')
|
|
Object.defineProperty(globalThis, 'CustomEvent', { value: FakeCustomEvent, writable: true, configurable: true, enumerable: true })
|
|
try {
|
|
await fn(fx)
|
|
await fx.ruhe()
|
|
} finally {
|
|
fx.restore()
|
|
if (davor) Object.defineProperty(globalThis, 'CustomEvent', davor)
|
|
else delete globalThis.CustomEvent
|
|
}
|
|
}
|
|
|
|
/** Lauscht am document.body auf kn:reiter. => { reiter: [detail, …] } (in der Reihenfolge des Eintreffens). */
|
|
export function hoereAufKn(fx) {
|
|
const k = { reiter: [] }
|
|
fx.document.body.addEventListener('kn:reiter', (ev) => { k.reiter.push(ev.detail) })
|
|
return k
|
|
}
|
|
|
|
/**
|
|
* Ersetzt fetch: Antworten nach 'METHODE /pfad?query'. Wert: { status?, body } | Array (der Reihe nach, der letzte gilt weiter) |
|
|
* (aufruf) => Wert. Unbekannte Adressen: 404 und Eintrag in fx.unbekannt. Jeder Aufruf steht in fx.fetchLog.
|
|
*/
|
|
export function routen(fx, tabelle) {
|
|
const felder = new Map(Object.entries(tabelle).map(([schluessel, wert]) => [schluessel, Array.isArray(wert) ? [...wert] : wert]))
|
|
fx.unbekannt = []
|
|
globalThis.fetch = async (url, optionen = {}) => {
|
|
const aufruf = { method: optionen.method || 'GET', url: String(url), body: optionen.body ? JSON.parse(optionen.body) : undefined }
|
|
fx.fetchLog.push(aufruf)
|
|
const schluessel = `${aufruf.method} ${aufruf.url}`
|
|
let eintrag = felder.get(schluessel)
|
|
if (Array.isArray(eintrag)) eintrag = eintrag.length > 1 ? eintrag.shift() : eintrag[0]
|
|
if (typeof eintrag === 'function') eintrag = eintrag(aufruf)
|
|
if (eintrag === undefined) {
|
|
fx.unbekannt.push(schluessel)
|
|
eintrag = { status: 404, body: { error: `nicht vorgesehen: ${schluessel}` } }
|
|
}
|
|
const { status = 200, body = {} } = eintrag
|
|
return { ok: status >= 200 && status < 300, status, json: async () => structuredClone(body) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attrappe fuer zeigeEinschalten. fn(platz, { key, projectId, viewAs, onFertig }) zeichnet nichts und merkt den Aufruf.
|
|
* fertig(i, ergebnis) ruft onFertig des i-ten Aufrufs.
|
|
*/
|
|
export function einschaltenAttrappe() {
|
|
const aufrufe = []
|
|
const fn = (wurzel, o = {}) => {
|
|
aufrufe.push({ wurzel, key: o.key, projectId: o.projectId, viewAs: o.viewAs, onFertig: o.onFertig })
|
|
return Promise.resolve()
|
|
}
|
|
const fertig = (i = 0, ergebnis = { feature: { state: 'setup' }, ticketWoid: null }) => aufrufe[i].onFertig(ergebnis)
|
|
return { fn, aufrufe, fertig }
|
|
}
|
|
|
|
function attrappe() {
|
|
const handlers = {}
|
|
return {
|
|
innerHTML: '',
|
|
textContent: '',
|
|
disabled: false,
|
|
checked: false,
|
|
value: '',
|
|
dataset: {},
|
|
classList: { add() {}, remove() {}, toggle() {} },
|
|
handlers,
|
|
addEventListener(typ, fn) { handlers[typ] = fn },
|
|
querySelector: () => null,
|
|
querySelectorAll: () => [],
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wurzel fuer die innerHTML-Module (News, Analyse): { root, stage, kn }. root.querySelector(stageSelektor) liefert stage, '.an' die Attrappe `an`.
|
|
* stage.els['#id'] entsteht beim ersten querySelector('#id'). root.dispatchEvent legt die Meldungen 'kn:reiter' (ev.detail) in kn.reiter ab.
|
|
*/
|
|
export function htmlWurzel(stageSelektor) {
|
|
const els = {}
|
|
const attribute = {}
|
|
const stage = {
|
|
els,
|
|
attribute,
|
|
innerHTML: '',
|
|
querySelector: (selektor) => (els[selektor] ||= attrappe()),
|
|
querySelectorAll: () => [],
|
|
setAttribute: (name, wert) => { attribute[name] = String(wert) },
|
|
getAttribute: (name) => (Object.hasOwn(attribute, name) ? attribute[name] : null),
|
|
}
|
|
const an = attrappe()
|
|
const kn = { reiter: [] }
|
|
const root = {
|
|
innerHTML: '',
|
|
dataset: {},
|
|
querySelector: (selektor) => (selektor === stageSelektor ? stage : selektor === '.an' ? an : null),
|
|
dispatchEvent(ev) {
|
|
if (ev?.type === 'kn:reiter') kn.reiter.push(ev.detail)
|
|
return true
|
|
},
|
|
}
|
|
return { root, stage, kn }
|
|
}
|
|
|
|
/**
|
|
* public/funktionen.js laden, alle statischen Importe (absolut /x.js oder relativ ./x.js) auf die Dateien in public/ umbiegen
|
|
* (Muster funktionenZeitenTile.test.js). => das Modul.
|
|
*/
|
|
export async function ladeFunktionenModul() {
|
|
const dir = mkdtempSync(join(tmpdir(), 'fn-modul-abo-'))
|
|
const dateiUrl = (name) => pathToFileURL(new URL(`../../public/${name}`, import.meta.url).pathname).href
|
|
const quelle = readFileSync(new URL('../../public/funktionen.js', import.meta.url), 'utf8')
|
|
const src = quelle.replace(/(\bfrom\s+)'(?:\/|\.\/)([^']+)'/g, (_, von, name) => `${von}'${dateiUrl(name)}'`)
|
|
const datei = join(dir, 'funktionen.mjs')
|
|
writeFileSync(datei, src)
|
|
return import(pathToFileURL(datei).href)
|
|
}
|