Terminplaner: Einbau-Skript – Nachbesserungen
429 ohne Schleife (Meldung und Erneut-versuchen-Knopf, Formulare bleiben stehen), Schriftfarbe auf der Betriebsfarbe mit mindestens 4,5 : 1, AGB-Link aus der oeffentlichen Konfiguration (betrieb.agbUrl), Haken starten bei geaenderter Auswahl ohne Haken, Fehler ohne Feld nicht verschluckt, Konfiguration fail closed, ladeNeu fuer Neuladen und neu_laden, Tastatur und Fokus (Modifikatoren, Monatspfeile, Belehrungstext), Aufraeumen. UI-Testserver: Timer, offene Verbindungen, Hinweis auf sessionCookie. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fef7e47f0
commit
4465ac7b94
9 files changed
+501
-88
No files matched your search
@@ -12,7 +12,7 @@ import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
startTermineUiServer, salonEingabe,
|
||||
R_ANNA, S_DAMEN, S_HERREN, S_KENNEN,
|
||||
R_ANNA, S_DAMEN, S_HERREN,
|
||||
} from './termineTestServer.mjs'
|
||||
|
||||
const PW = process.env.PLAYWRIGHT_CORE || '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
|
||||
@@ -165,10 +165,17 @@ async function pruefungen() {
|
||||
assert(zus.replace(/\s+/g, ' ').includes(t), `Zusammenfassung ohne "${t}": ${zus}`)
|
||||
}
|
||||
assert((await wd.locator('.wkt-box').innerText()).includes('Danach berechnen wir 50 % des Preises.'), 'Stornoregel im Klartext fehlt')
|
||||
assert(zus.replace(/\s+/g, ' ').includes('Beschreibung Mit Beratung zur Frisur.'), 'Beschreibung der Leistung fehlt in der Zusammenfassung')
|
||||
const agb = wd.locator('.wkt-box a')
|
||||
eq([await agb.innerText(), await agb.getAttribute('href'), await agb.getAttribute('rel'), await agb.getAttribute('target')],
|
||||
['AGB des Betriebs', 'https://salon-mueller.example/agb', 'noopener noreferrer', '_blank'], 'Link auf die AGB neben den Stornobedingungen')
|
||||
const det = wd.locator('details.wkt-belehrung')
|
||||
assert(await det.count() === 1 && await det.evaluate((d) => !d.open), 'Widerrufsbelehrung muss aufklappbar und zu sein')
|
||||
await det.locator('summary').click()
|
||||
assert((await det.innerText()).includes('Widerrufsbelehrung') && (await det.innerText()).includes('Muster-Widerrufsformular'), 'Belehrung nicht lesbar')
|
||||
const belehrung = det.locator('.wkt-belehrung-text')
|
||||
eq([await belehrung.getAttribute('tabindex'), await belehrung.getAttribute('role'), await belehrung.getAttribute('aria-label')],
|
||||
['0', 'region', 'Widerrufsbelehrung und Muster-Widerrufsformular'], 'Belehrungstext per Tastatur scrollbar')
|
||||
await det.locator('summary').click()
|
||||
assert((await wd.locator('.wkt-datenschutz').innerText()).startsWith('Datenschutz. Wir verwenden Ihre Angaben'), 'Datenschutz-Hinweis')
|
||||
eq(await wd.locator('.wkt-datenschutz a').getAttribute('rel'), 'noopener noreferrer', 'rel am Datenschutz-Link')
|
||||
@@ -366,6 +373,8 @@ async function pruefungen() {
|
||||
const aktTag = () => page.evaluate(() => document.activeElement.dataset.tag)
|
||||
const monat = () => wd.locator('.wkt-monat').innerText()
|
||||
eq(await aktTag(), '2026-10-05', 'Start')
|
||||
await page.keyboard.press('Control+ArrowRight'); eq(await aktTag(), '2026-10-05', 'Strg+Pfeil gehoert dem Browser, nicht dem Raster')
|
||||
await page.keyboard.press('Meta+ArrowDown'); eq(await aktTag(), '2026-10-05', 'Meta+Pfeil gehoert dem System, nicht dem Raster')
|
||||
await page.keyboard.press('ArrowRight'); eq(await aktTag(), '2026-10-06', 'Pfeil rechts')
|
||||
await page.keyboard.press('ArrowDown'); eq(await aktTag(), '2026-10-13', 'Pfeil runter = +7')
|
||||
await page.keyboard.press('ArrowLeft'); eq(await aktTag(), '2026-10-12', 'Pfeil links')
|
||||
@@ -797,6 +806,238 @@ async function pruefungen() {
|
||||
eq(fremd, [], 'Anfragen an fremde Hosts')
|
||||
})
|
||||
|
||||
|
||||
// ---------------------------------------------------------------- 13. Nachbesserungen (Fix-Runde 1)
|
||||
|
||||
const ACAO = { 'access-control-allow-origin': '*' }
|
||||
const TEXT_429 = 'Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.'
|
||||
const json = (route, status, body) => route.fulfill({ status, contentType: 'application/json', headers: ACAO, body: JSON.stringify(body) })
|
||||
|
||||
await check('grenze_429_keine_schleife', async (page) => {
|
||||
// lesend (Tage): Meldung und Knopf, kein neuer Abruf von selbst
|
||||
let tage = 0
|
||||
await page.route(/\/tage\?/, (route) => { tage += 1; return json(route, 429, { fehler: { _: TEXT_429 } }) })
|
||||
await oeffne(page, seitenHtml(eingebettet('data-modus="inline"')))
|
||||
const wd = w(page)
|
||||
await karte(wd, 'Herrenschnitt').click()
|
||||
await karte(wd, 'Egal').click()
|
||||
const meldung = wd.locator('.wkt-ausfall')
|
||||
await meldung.waitFor()
|
||||
await page.waitForTimeout(1500)
|
||||
assert(tage >= 1 && tage <= 2, `bei 429 darf es hoechstens 2 Abrufe von /tage geben, es waren ${tage}`)
|
||||
eq(await meldung.innerText(), TEXT_429, 'Meldung der Grenze')
|
||||
assert(await wd.getByRole('button', { name: 'Erneut versuchen' }).isVisible(), 'Knopf "Erneut versuchen" fehlt')
|
||||
await shot(page, 'fehler-429-lesen')
|
||||
await page.unroute(/\/tage\?/)
|
||||
await wd.getByRole('button', { name: 'Erneut versuchen' }).click()
|
||||
await wd.locator('td.wkt-tag').first().waitFor()
|
||||
|
||||
// schreibend (Buchung): die Zusammenfassung bleibt mit der Meldung stehen
|
||||
await tag(wd, '2026-10-26').click()
|
||||
await zeit(wd, '10:00').click()
|
||||
await wd.locator('input[name=name]').fill('Max Mustermann')
|
||||
await wd.locator('input[name=email]').fill('max@example.com')
|
||||
await wd.getByRole('button', { name: 'Weiter' }).click()
|
||||
await wd.locator('.wkt-zusammen').waitFor()
|
||||
await wd.locator('input[name=storno]').check()
|
||||
let buchungen = 0
|
||||
await page.route(/\/buchungen$/, (route) => { buchungen += 1; return json(route, 429, { fehler: { _: TEXT_429 } }) })
|
||||
await wd.getByRole('button', { name: 'Zahlungspflichtig buchen' }).click()
|
||||
await wd.locator('.wkt-fehlerbox .wkt-fehlertext').waitFor()
|
||||
await page.waitForTimeout(800)
|
||||
eq(await wd.locator('.wkt-fehlerbox .wkt-fehlertext').innerText(), TEXT_429, 'Meldung in der Zusammenfassung')
|
||||
eq(await titel(wd).innerText(), 'Bitte prüfen Sie Ihre Buchung', 'bleibt in der Zusammenfassung')
|
||||
eq(buchungen, 1, 'genau ein Versuch')
|
||||
assert(await wd.locator('input[name=storno]').isChecked(), 'Haken bleibt')
|
||||
await page.unroute(/\/buchungen$/)
|
||||
|
||||
// Widerrufsformular (auch eine 429 ohne Text): bleibt in der Widerrufsansicht
|
||||
let widerrufe = 0
|
||||
await page.route(/\/widerruf$/, (route) => { widerrufe += 1; return json(route, 429, {}) })
|
||||
await wd.getByRole('button', { name: 'Vertrag widerrufen' }).click()
|
||||
await warteTitel(wd, 'Vertrag widerrufen')
|
||||
await wd.locator('input[name=name]').fill('Gerda Gast')
|
||||
await wd.locator('input[name=email]').fill('gerda@example.com')
|
||||
await wd.getByRole('button', { name: 'Widerruf bestätigen' }).click()
|
||||
await wd.locator('.wkt-fehlerbox .wkt-fehlertext').waitFor()
|
||||
await page.waitForTimeout(800)
|
||||
eq(await wd.locator('.wkt-fehlerbox .wkt-fehlertext').innerText(), TEXT_429, 'Meldung im Widerrufsformular')
|
||||
eq(await titel(wd).innerText(), 'Vertrag widerrufen', 'bleibt in der Widerrufsansicht')
|
||||
eq(widerrufe, 1, 'genau ein Versuch')
|
||||
assert(await wd.getByRole('button', { name: 'Widerruf bestätigen' }).isEnabled(), 'Knopf wieder frei')
|
||||
assert(await wd.locator('input[name=name]').inputValue() === 'Gerda Gast', 'Eingaben bleiben')
|
||||
await shot(page, 'fehler-429-widerruf')
|
||||
await wd.getByRole('button', { name: 'Zurück zur Terminbuchung' }).click()
|
||||
await wd.locator('.wkt-zusammen').waitFor()
|
||||
})
|
||||
|
||||
await check('betriebsfarbe_orange_schrift_lesbar', async (page) => {
|
||||
await oeffne(page, seitenHtml(eingebettet(`data-modus="inline" data-leistung="${S_HERREN}"`)))
|
||||
const wd = await bisZusammenfassung(page, { leistung: null, tag: '2026-10-26' })
|
||||
const farben = await wd.locator('.wkt-form button[type=submit]').evaluate((b) => [getComputedStyle(b).backgroundColor, getComputedStyle(b).color])
|
||||
eq(farben, ['rgb(234, 88, 12)', 'rgb(17, 24, 39)'], 'Orange bekommt dunkle Schrift (Weiss waere nur 3,6 : 1)')
|
||||
await shot(page, 'orange-pruefen')
|
||||
}, { eingabe: salonEingabe({ farbe: '#ea580c' }) })
|
||||
|
||||
await check('haken_beginnen_bei_aenderung_ohne_haken', async (page) => {
|
||||
await oeffne(page, seitenHtml(eingebettet('data-modus="inline"')))
|
||||
const wd = await bisZusammenfassung(page, { leistung: 'Herrenschnitt', tag: '2026-10-12', zeit: '10:00' })
|
||||
const haken = () => wd.locator('.wkt-haken input').evaluateAll((l) => l.map((i) => i.name + ':' + i.checked))
|
||||
const alleAn = async () => { for (const n of ['storno', 'vorzeitig', 'erinnerung']) await wd.locator(`input[name=${n}]`).check() }
|
||||
const zurueck = async (n) => { for (let i = 0; i < n; i += 1) await wd.getByRole('button', { name: 'Zurück' }).click() }
|
||||
const weiterBisZusammenfassung = async () => { await wd.getByRole('button', { name: 'Weiter' }).click(); await wd.locator('.wkt-zusammen').waitFor() }
|
||||
await alleAn()
|
||||
eq(await haken(), ['storno:true', 'vorzeitig:true', 'erinnerung:true'], 'alle drei angekreuzt')
|
||||
// dieselbe Uhrzeit noch einmal gewaehlt: nichts hat sich geaendert, die Haken bleiben
|
||||
await zurueck(2)
|
||||
await wd.locator('.wkt-zeit').first().waitFor()
|
||||
await zeit(wd, '10:00').click()
|
||||
await warteTitel(wd, 'Ihre Angaben')
|
||||
await weiterBisZusammenfassung()
|
||||
eq(await haken(), ['storno:true', 'vorzeitig:true', 'erinnerung:true'], 'gleiche Uhrzeit: Haken bleiben')
|
||||
// andere Uhrzeit
|
||||
await zurueck(2)
|
||||
await wd.locator('.wkt-zeit').first().waitFor()
|
||||
await zeit(wd, '10:15').click()
|
||||
await warteTitel(wd, 'Ihre Angaben')
|
||||
await weiterBisZusammenfassung()
|
||||
eq(await haken(), ['storno:false', 'vorzeitig:false', 'erinnerung:false'], 'andere Uhrzeit: ohne Haken')
|
||||
// anderer Tag
|
||||
await alleAn()
|
||||
await zurueck(2)
|
||||
await tag(wd, '2026-10-13').click()
|
||||
await zeit(wd, '10:15').click()
|
||||
await warteTitel(wd, 'Ihre Angaben')
|
||||
await weiterBisZusammenfassung()
|
||||
eq(await haken(), ['storno:false', 'vorzeitig:false', 'erinnerung:false'], 'anderer Tag: ohne Haken')
|
||||
// andere Leistung
|
||||
await alleAn()
|
||||
await zurueck(4)
|
||||
await warteTitel(wd, 'Welche Leistung')
|
||||
await karte(wd, 'Kinderschnitt').click()
|
||||
await karte(wd, 'Egal').click()
|
||||
await tag(wd, '2026-10-13').click()
|
||||
await zeit(wd, '10:15').click()
|
||||
await warteTitel(wd, 'Ihre Angaben')
|
||||
await weiterBisZusammenfassung()
|
||||
eq(await haken(), ['storno:false', 'vorzeitig:false', 'erinnerung:false'], 'andere Leistung: ohne Haken')
|
||||
})
|
||||
|
||||
await check('fehler_ohne_feld_nicht_verschluckt', async (page) => {
|
||||
let fehler = {}
|
||||
await page.route(/\/buchungen$/, (route) => json(route, 400, { fehler }))
|
||||
await oeffne(page, seitenHtml(eingebettet('data-modus="inline"')))
|
||||
const wd = await bisZusammenfassung(page, { leistung: 'Herrenschnitt', tag: '2026-10-26' })
|
||||
await wd.locator('input[name=storno]').check()
|
||||
eq(await wd.locator('input[name=vorzeitig]').count(), 0, 'Voraussetzung: der Haken wird nicht gezeigt')
|
||||
// 1) Schluessel ohne Feld in der Zusammenfassung: Fehlerkasten
|
||||
fehler = { vorzeitig: 'Bitte der Leistung vor Ablauf der Widerrufsfrist ausdrücklich zustimmen, sonst ist die Buchung nicht möglich.', irgendwas: 'Unbekannter Fehler vom Server.' }
|
||||
await wd.getByRole('button', { name: 'Zahlungspflichtig buchen' }).click()
|
||||
await wd.locator('.wkt-fehlerbox .wkt-fehlertext').first().waitFor()
|
||||
eq(await wd.locator('.wkt-fehlerbox .wkt-fehlertext').allInnerTexts(), Object.values(fehler), 'beide Meldungen sichtbar')
|
||||
eq(await titel(wd).innerText(), 'Bitte prüfen Sie Ihre Buchung', 'bleibt in der Zusammenfassung')
|
||||
// 2) Feld im Schritt "Angaben" plus Schluessel ohne Feld
|
||||
fehler = { name: 'Bitte einen Namen angeben.', foo: 'Weitere Meldung.' }
|
||||
await wd.getByRole('button', { name: 'Zahlungspflichtig buchen' }).click()
|
||||
await warteTitel(wd, 'Ihre Angaben')
|
||||
eq(await wd.locator('input[name=name]').getAttribute('aria-invalid'), 'true', 'Namensfeld markiert')
|
||||
eq(await wd.locator('.wkt-fehlerbox .wkt-fehlertext').allInnerTexts(), ['Weitere Meldung.'], 'Meldung ohne Feld im Kasten')
|
||||
// 3) Leistung: der Leistungsschritt zeigt die Meldung
|
||||
await wd.getByRole('button', { name: 'Weiter' }).click()
|
||||
await wd.locator('.wkt-zusammen').waitFor()
|
||||
fehler = { leistung: 'Bitte eine Leistung auswählen.' }
|
||||
await wd.getByRole('button', { name: 'Zahlungspflichtig buchen' }).click()
|
||||
await warteTitel(wd, 'Welche Leistung')
|
||||
eq(await wd.locator('.wkt-fehlerbox .wkt-fehlertext').allInnerTexts(), ['Bitte eine Leistung auswählen.'], 'Meldung im Leistungsschritt')
|
||||
})
|
||||
|
||||
await check('angebot_geaendert_beginnt_von_vorn', async (page) => {
|
||||
await oeffne(page, seitenHtml(eingebettet('data-modus="inline"')))
|
||||
const wd = await bisZusammenfassung(page, { leistung: 'Damenschnitt', tag: '2026-10-26' })
|
||||
await wd.locator('input[name=storno]').check()
|
||||
// der Betrieb nimmt die Leistung heraus; das Formular-Token ist inzwischen abgelaufen (neu_laden)
|
||||
const eingabe = salonEingabe()
|
||||
eingabe.leistungen = eingabe.leistungen.filter((l) => l.id !== S_DAMEN)
|
||||
await srv.speichere(eingabe)
|
||||
srv.schiebeUhr(3 * 60 * 60 * 1000)
|
||||
await wd.getByRole('button', { name: 'Zahlungspflichtig buchen' }).click()
|
||||
await wd.locator('.wkt-meldung').waitFor()
|
||||
eq(await wd.locator('.wkt-meldung').innerText(), 'Das Angebot hat sich geändert. Bitte wählen Sie neu.', 'Hinweis')
|
||||
eq(await titel(wd).innerText(), 'Welche Leistung möchten Sie buchen?', 'zurueck zur Leistungswahl')
|
||||
eq(await karte(wd, 'Damenschnitt').count(), 0, 'die entfernte Leistung ist weg')
|
||||
eq(srv.buchungen().length, 0, 'nichts gebucht')
|
||||
})
|
||||
|
||||
await check('konfiguration_unvollstaendig_ausweichtext', async (page, ctx) => {
|
||||
const varianten = {
|
||||
'Leistung ohne Knopftext': (j) => { delete j.leistungen[0].knopf },
|
||||
'ohne Text fuer den Storno-Haken': (j) => { delete j.texte.storno },
|
||||
'ohne Text fuer den Haken vorzeitiger Beginn': (j) => { delete j.texte.vorzeitig },
|
||||
'Widerruf ohne Belehrung': (j) => { delete j.texte.widerrufsbelehrung },
|
||||
'ohne Formular-Token': (j) => { delete j.formToken },
|
||||
}
|
||||
for (const [name, aendere] of [['unveraendert', () => {}], ...Object.entries(varianten)]) {
|
||||
const p = await ctx.newPage()
|
||||
p.setDefaultTimeout(8000)
|
||||
await p.route(/\/config$/, async (route) => {
|
||||
const antwort = await route.fetch()
|
||||
const daten = await antwort.json()
|
||||
aendere(daten)
|
||||
await route.fulfill({ response: antwort, json: daten })
|
||||
})
|
||||
await p.goto(srv.seite('unvollstaendig.html', seitenHtml(eingebettet('data-modus="inline"'))))
|
||||
if (name === 'unveraendert') {
|
||||
await karte(w(p), 'Herrenschnitt').waitFor()
|
||||
} else {
|
||||
await p.waitForLoadState('networkidle')
|
||||
await p.waitForTimeout(300)
|
||||
assert(await p.locator('#ausweich').isVisible(), `${name}: Ausweichtext muss stehen`)
|
||||
eq(await p.locator('.wkt-root').count(), 0, `${name}: kein Widget`)
|
||||
}
|
||||
await p.close()
|
||||
}
|
||||
})
|
||||
|
||||
await check('render_fehler_zeigt_ausweichtext_wieder', async (page) => {
|
||||
// Der Aufbau der Leistungsliste scheitert (<ul> laesst sich nicht erzeugen), nachdem der Ausweichtext schon beiseite ist
|
||||
const kaputt = await page.context().newPage()
|
||||
await kaputt.addInitScript(() => {
|
||||
const echt = document.createElement.bind(document)
|
||||
document.createElement = (tag, opt) => { if (String(tag).toLowerCase() === 'ul') throw new Error('Test: Aufbau scheitert'); return echt(tag, opt) }
|
||||
})
|
||||
const fehler = []
|
||||
kaputt.on('pageerror', (e) => fehler.push(e.message))
|
||||
await kaputt.goto(srv.seite('kaputt.html', seitenHtml(eingebettet('data-modus="inline"'))))
|
||||
await kaputt.waitForLoadState('networkidle')
|
||||
await kaputt.waitForTimeout(500)
|
||||
assert(await kaputt.locator('#ausweich').isVisible(), 'Ausweichtext muss nach dem Fehler wieder da sein')
|
||||
eq(await kaputt.locator('.wkt-root').count(), 0, 'kein halbes Widget')
|
||||
eq(fehler, [], 'der Fehler wird abgefangen')
|
||||
await kaputt.close()
|
||||
})
|
||||
|
||||
await check('monatspfeil_behaelt_fokus', async (page) => {
|
||||
await oeffne(page, seitenHtml(eingebettet(`data-modus="inline" data-leistung="${S_HERREN}"`)))
|
||||
const wd = w(page)
|
||||
await karte(wd, 'Egal').click()
|
||||
await wd.locator('td.wkt-tag').first().waitFor()
|
||||
const vor = wd.getByRole('button', { name: 'Nächster Monat' })
|
||||
const zur = wd.getByRole('button', { name: 'Vorheriger Monat' })
|
||||
const fokus = () => page.evaluate(() => { const a = document.activeElement; return { body: a === document.body, label: a.getAttribute('aria-label') || '', imWidget: !!a.closest('[data-webklar-termine]') } })
|
||||
const klickeBisGesperrt = async (knopf) => {
|
||||
for (let i = 0; i < 8 && await knopf.isEnabled(); i += 1) {
|
||||
const vorher = await wd.locator('.wkt-monat').innerText()
|
||||
await knopf.click()
|
||||
await page.waitForFunction((v) => document.querySelector('.wkt-monat').textContent !== v, vorher)
|
||||
}
|
||||
assert(await knopf.isDisabled(), 'Pfeil wurde nie gesperrt')
|
||||
}
|
||||
await klickeBisGesperrt(vor)
|
||||
eq(await fokus(), { body: false, label: 'Vorheriger Monat', imWidget: true }, 'Fokus nach dem gesperrten "Naechster Monat" beim anderen Pfeil')
|
||||
await klickeBisGesperrt(zur)
|
||||
eq(await fokus(), { body: false, label: 'Nächster Monat', imWidget: true }, 'Fokus nach dem gesperrten "Vorheriger Monat" beim anderen Pfeil')
|
||||
})
|
||||
|
||||
// Zum Schluss: Server gestoppt. Seite und Skript liefert ein eigener kleiner Server, die Schnittstelle ist weg (Verbindung abgelehnt).
|
||||
await check('server_gestoppt_ausweichtext', async (page) => {
|
||||
const skript = await readFile(WIDGET_JS, 'utf8')
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
// damit Tage und Uhrzeiten in Screenshots gleich bleiben und Zwischenspeicher, Haltezeiten und Formular-Token trotzdem
|
||||
// ablaufen. srv.schiebeUhr(ms) verschiebt sie (z. B. +3 h: Formular-Token abgelaufen), srv.setzeUhr(date) setzt sie neu.
|
||||
//
|
||||
// Cookies fuer das Portal (Aufgaben 21a/21b): sessionCookie/customerCookie aus test/support/testApp.js und fixtures.js erst NACH dem
|
||||
// Start dieses Servers dynamisch importieren (testApp.js prueft beim Laden, dass die Umgebung auf die Nachbauten zeigt):
|
||||
// const srv = await startTermineUiServer()
|
||||
// const { sessionCookie } = await import('../support/testApp.js')
|
||||
//
|
||||
// Seitenherkunft: srv.url ist http://127.0.0.1:PORT, srv.pageOrigin ist http://localhost:PORT (derselbe Server, anderer Host).
|
||||
// Mit { crossOrigin: true } gilt pageOrigin als Herkunft des Portals (config.publicBaseUrl): Seiten von pageOrigin mit
|
||||
// data-api=srv.url sind dann echte Cross-Origin-Anfragen (Preflight fuer POST), die der Server erlaubt.
|
||||
@@ -64,6 +69,7 @@ export function salonEingabe(ueberschreibe = {}) {
|
||||
websiteUrl: 'https://salon-mueller.example',
|
||||
impressumUrl: 'https://salon-mueller.example/impressum',
|
||||
datenschutzUrl: 'https://salon-mueller.example/datenschutz',
|
||||
agbUrl: 'https://salon-mueller.example/agb',
|
||||
},
|
||||
anrede: 'sie',
|
||||
farbe: '#0f766e',
|
||||
@@ -259,7 +265,11 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
|
||||
async beendeApp() {
|
||||
if (srv.appBeendet) return
|
||||
srv.appBeendet = true
|
||||
await Promise.race([app.close(), new Promise((resolve) => setTimeout(resolve, 3000))])
|
||||
let frist = null
|
||||
const geschlossen = app.close()
|
||||
app.server.closeAllConnections?.() // offene Verbindungen des Browsers (Keep-Alive) halten close() sonst auf
|
||||
await Promise.race([geschlossen, new Promise((resolve) => { frist = setTimeout(resolve, 3000) })])
|
||||
clearTimeout(frist)
|
||||
},
|
||||
appBeendet: false,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user