Files
Webklar-Kundenbereich/test/support/knWortlautOrakel.js
T

126 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Wortlaut-Orakel (P2): lädt das alte public/app.js in eine VM, rendert die vier Wortlaut-Seiten (Bestellen, Wechsel, E-Mail-Speicher,
// Gutschrift) mit Attrappen und liefert deren HTML. Die neuen Seiten müssen denselben sichtbaren Text haben (C14, Fassung 5).
// Nur lesend: app.js bleibt unverändert. P10 friert dieses Hilfsmittel vor dem Löschen von app.js ein (N40, N51).
import vm from 'node:vm'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { setImmediate as takt } from 'node:timers/promises'
const APP_JS = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'app.js')
const ENTITAETEN = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'", '&nbsp;': ' ' }
/** Sichtbarer Text eines HTML-Strings oder Elements: ohne Tags, Kommentare, <script>, <style> und ohne jeden Leerraum (auch U+00A0). */
export function sichtbarerText(quelle) {
const roh = typeof quelle === 'string' ? quelle : String(quelle?.textContent ?? '')
const ohneTags = typeof quelle === 'string'
? roh
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, '')
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, '')
.replace(/<[^>]*>/g, '')
: roh
return ohneTags.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (e) => ENTITAETEN[e]).replace(/\s+/g, '')
}
/** Streicht den Seitenkopf <header class="kb-head">…</header> (Titel zeichnet die Hülle, Untertitel entfallen); ohne Kopf bleibt alles. */
export function ohneKopf(html) {
return String(html).replace(/<header class="kb-head">[\s\S]*?<\/header>/, '')
}
function attrappeElement() {
return {
innerHTML: '',
textContent: '',
dataset: {},
style: {},
value: '',
checked: false,
disabled: false,
classList: { contains: () => false, add() {}, remove() {}, toggle() {} },
querySelector: () => null,
querySelectorAll: () => [],
addEventListener() {},
removeEventListener() {},
insertAdjacentHTML() {},
scrollIntoView() {},
focus() {},
}
}
/** app.js in einer VM mit Attrappen laden. Alle Top-Level-Berührungen sind addEventListener, matchMedia, querySelector, navigator und serviceWorker. */
export function ladeAlteApp() {
const antworten = new Map()
const fetchAttrappe = async (url) => {
const pfad = String(url).split('?')[0]
const body = antworten.has(pfad) ? antworten.get(pfad) : {}
return { ok: antworten.has(pfad), status: antworten.has(pfad) ? 200 : 404, json: async () => structuredClone(body) }
}
const medien = () => ({ matches: false, addEventListener() {}, removeEventListener() {}, addListener() {} })
const elemente = new Map()
const document = {
body: attrappeElement(),
addEventListener() {},
querySelector: () => null,
querySelectorAll: () => [],
getElementById: (id) => elemente.get(id) ?? null,
createElement: () => attrappeElement(),
}
const window = {
addEventListener() {},
removeEventListener() {},
matchMedia: medien,
location: { href: '', hash: '', search: '', pathname: '/dashboard.html' },
history: { state: null, replaceState() {}, pushState() {}, back() {} },
isSecureContext: false,
scrollTo() {},
}
class CustomEventAttrappe {
constructor(typ, init = {}) { this.type = typ; this.detail = init.detail ?? null }
}
const sandbox = {
window, document, navigator: { userAgent: '' }, fetch: fetchAttrappe, CustomEvent: CustomEventAttrappe, Event: CustomEventAttrappe,
console, URLSearchParams, TextEncoder, structuredClone, setTimeout, clearTimeout, setInterval, clearInterval,
}
const ctx = vm.createContext(sandbox)
vm.runInContext(readFileSync(APP_JS, 'utf8'), ctx, { filename: 'app.js' })
const view = () => attrappeElement()
return {
/** Bestellseite (#bestellen-hosting / #bestellen-wartung): HTML der Ansicht. */
bestellen(daten, key) {
const v = view()
vm.runInContext('renderOrderView', ctx)(v, daten, key)
return v.innerHTML
},
/** Wechselseite (#wechsel) nach der Vorschau-Antwort. */
async wechsel(daten, vorschau) {
antworten.set('/api/abo/switch/preview', vorschau)
const v = view()
vm.runInContext('renderSwitchView', ctx)(v, daten)
await takt()
await takt()
return v.innerHTML
},
/** Speicher-Block der Verträge-Ansicht; die Vorschau kommt bei geplanter Reduzierung (app.js:1405). */
async speicher(daten, vorschau) {
if (vorschau) antworten.set('/api/abo/storage/preview', vorschau)
const c = view()
vm.runInContext('renderStoragePanel', ctx)(c, daten)
await takt()
await takt()
return c.innerHTML
},
/** Gutschrift-Karte (#gutschrift) mit der Antwort von GET /api/abo/takeover-credits. */
async gutschrift(daten, antwort) {
antworten.set('/api/abo/takeover-credits', antwort)
const karte = view()
elemente.set('takeover-card', karte)
vm.runInContext('takeoverState.customer = { email: "anna@example.com" }', ctx)
await vm.runInContext('loadTakeoverCard', ctx)({ abo: daten })
return karte.innerHTML
},
}
}