diff --git a/public/embed/termine.js b/public/embed/termine.js index c0fb06c..53a7488 100644 --- a/public/embed/termine.js +++ b/public/embed/termine.js @@ -1275,6 +1275,12 @@ }, function (e2) { fehlerAllgemein(e2, function () { zeichne(true) }) }) return } + if (angebotFehler(err)) { + // Leistung oder Person unbekannt: das Angebot hat sich vermutlich geaendert. Erst frisch laden; gilt die Auswahl nicht mehr, beginnt + // ladeNeu() von vorn (mit Hinweis), sonst steht die Meldung des Servers. + ladeNeu().then(function (weiter) { if (weiter) zeigeServerFehler(err.fehler) }, function (e2) { fehlerAllgemein(e2, function () { zeichne(true) }) }) + return + } mitMeldung429(err) if (err && !err.netz && err.fehler && Object.keys(err.fehler).length) { zeigeServerFehler(err.fehler) @@ -1307,11 +1313,24 @@ return (err.fehler && err.fehler._) || T('Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.', 'Zu viele Anfragen. Bitte versuche es später noch einmal.') } + // 400 des Servers mit Fehler an leistung oder ressource (nicht aus dem Netz): die Auswahl passt nicht mehr zum Angebot + function angebotFehler (err) { + return !!(err && !err.netz && err.fehler && (err.fehler.leistung || err.fehler.ressource)) + } + // Fehler eines lesenden Abrufs (Konfiguration, Tage, Zeiten) oder Netzfehler: keine Wiederholung von selbst (429 wuerde sonst // in einer Schleife weiterzaehlen), nur Meldung und "Erneut versuchen". function fehlerAllgemein (err, wiederhole) { if (err && err.inaktiv) { zeigeAusweich(); return } if (err && err.status === 429) { ausfall(wiederhole, meldung429(err)); return } + if (angebotFehler(err)) { + // Veraltetes Angebot (z. B. Leistung entfernt): frisch laden, ladeNeu() beginnt bei Aenderung von vorn mit Hinweis. Gilt die + // Auswahl noch, steht die Meldung mit "Erneut versuchen" (kein Neuaufbau, der denselben Abruf von selbst wiederholen wuerde). + ladeNeu().then(function (weiter) { + if (weiter) ausfall(wiederhole, Object.keys(err.fehler).map(function (k) { return err.fehler[k] }).join(' ')) + }, function (e2) { fehlerAllgemein(e2, wiederhole) }) + return + } ausfall(wiederhole) } @@ -1448,7 +1467,11 @@ ui.body.appendChild(l) l.focus() lade().then(function () { - starte(true) + try { + starte(true) + } catch (e) { + zeigeAusweich() // Aufbau scheitert: der Ausweichtext statt "Wird geladen …" + } }, function (err) { if (err && err.inaktiv) { zeigeAusweich(); return } ui.body.textContent = '' diff --git a/test/ui/termine-widget.ui.mjs b/test/ui/termine-widget.ui.mjs index 1fc572d..86c5350 100644 --- a/test/ui/termine-widget.ui.mjs +++ b/test/ui/termine-widget.ui.mjs @@ -1038,6 +1038,76 @@ async function pruefungen() { eq(await fokus(), { body: false, label: 'Nächster Monat', imWidget: true }, 'Fokus nach dem gesperrten "Vorheriger Monat" beim anderen Pfeil') }) + // ---------------------------------------------------------------- 14. Fix-Runde 2: veraltetes Angebot, Aufbaufehler im Knopfmodus + + // Die Auswahl wird gewaehlt, der Betrieb aendert das Angebot, dann antwortet der Server auf /tage mit 400 (leistung/ressource): + // das Widget muss frisch laden und von vorn beginnen (Hinweis, neue Liste), nicht "Gerade nicht erreichbar" zeigen. + const veraltetVorTage = (wahl) => async (page) => { + await oeffne(page, seitenHtml(eingebettet('data-modus="inline"'))) + const wd = w(page) + let freigeben = null + const tor = new Promise((resolve) => { freigeben = resolve }) + await page.route(/\/tage\?/, async (route) => { await tor; await route.continue() }) + await karte(wd, 'Damenschnitt').click() + await warteTitel(wd, 'Bei wem') + const eingabe = salonEingabe() + if (wahl === 'leistung') { + eingabe.leistungen = eingabe.leistungen.filter((l) => l.id !== S_DAMEN) + await karte(wd, 'Egal').click() // /tage ist unterwegs und wird festgehalten ... + } else { + eingabe.ressourcen = eingabe.ressourcen.map((r) => (r.id === R_ANNA ? { ...r, aktiv: false } : r)) + await karte(wd, 'Anna').click() + } + await srv.speichere(eingabe) // ... der Betrieb aendert das Angebot ... + freigeben() // ... und der Abruf erreicht den Server erst jetzt + 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?', 'Neubeginn bei der Leistungswahl') + eq(await wd.locator('.wkt-ausfall').count(), 0, 'kein "Gerade nicht erreichbar"') + eq(await karte(wd, 'Herrenschnitt').count(), 1, 'frische Liste') + if (wahl === 'leistung') { + eq(await karte(wd, 'Damenschnitt').count(), 0, 'entfernte Leistung nicht mehr in der Liste') + } else { + // mit Anna ausser Betrieb bleibt nur Ben: kein Personenschritt mehr, die Strecke geht gleich zum Termin + await karte(wd, 'Damenschnitt').click() + await warteTitel(wd, 'Wann passt es Ihnen?') + eq(await wd.locator('.wkt-pfad').innerText(), 'Schritt 2 von 4', 'ohne Personenschritt') + await wd.locator('td.wkt-tag[aria-disabled="false"]').first().waitFor() + } + await shot(page, `angebot-geaendert-${wahl}`) + } + await check('angebot_geaendert_leistung_vor_tage', veraltetVorTage('leistung')) + await check('angebot_geaendert_person_vor_tage', veraltetVorTage('person')) + + await check('angebot_geaendert_400_beim_absenden', 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() + const eingabe = salonEingabe() + eingabe.leistungen = eingabe.leistungen.filter((l) => l.id !== S_DAMEN) + await srv.speichere(eingabe) // das Token ist noch gueltig: der Server antwortet mit 400 fuer die Leistung, nicht mit neu_laden + 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?', 'Neubeginn') + eq(await karte(wd, 'Damenschnitt').count(), 0, 'entfernte Leistung nicht mehr in der Liste') + eq(srv.buchungen().length, 0, 'nichts gebucht') + }) + + await check('knopf_render_fehler_zeigt_ausweichtext', async (page) => { + await page.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) } + }) + await oeffne(page, seitenHtml(eingebettet('data-modus="knopf" data-knopf="Termin buchen"'))) + await page.getByRole('button', { name: 'Termin buchen' }).click() + const dlg = page.locator('dialog.wkt-dialog') + await dlg.locator('#ausweich').waitFor() + assert(await dlg.locator('#ausweich').isVisible(), 'Ausweichtext im Dialog') + assert(!(await dlg.innerText()).includes('Wird geladen'), '"Wird geladen" darf nicht stehen bleiben') + eq(await dlg.locator('.wkt-karte').count(), 0, 'kein halbes Widget') + }) + // 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')