diff --git a/public/neu/ansicht-bestellen.js b/public/neu/ansicht-bestellen.js index 4b54ca8..8b7b917 100644 --- a/public/neu/ansicht-bestellen.js +++ b/public/neu/ansicht-bestellen.js @@ -99,7 +99,7 @@ function bestellung(daten, productKey, { viewAs, kundenart }) { ], { id: 'abo-einzelheiten' }) const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null - const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true }) + const fehler = h('p', { class: K.fehler, id: 'abo-error', role: 'alert', hidden: true }) const kaufen = h('button', { type: 'button', class: K.primaer, id: 'abo-buy', disabled: viewAs }, T.knopf || KNOPF) const zeigeFehler = (text) => { fehler.textContent = text; fehler.hidden = false } diff --git a/public/neu/ansicht-gutschrift.js b/public/neu/ansicht-gutschrift.js index c07a124..ef50b7d 100644 --- a/public/neu/ansicht-gutschrift.js +++ b/public/neu/ansicht-gutschrift.js @@ -77,7 +77,7 @@ function antragsFormular(grenzen, email, T) { h('p', { class: 'kn-hinweis' }, 'Tipp: Lade auch die Kündigungsbestätigung deines bisherigen Anbieters hoch – daraus sehen wir, wann der Vertrag dort endet.'), feld('E-Mail-Adresse für unsere Antwort', h('input', { id: 'tk-email', class: K.input, type: 'email', maxlength: 255, autocomplete: 'email', value: email })), baueLinkzeile(T.gutschrift.satz, { id: 'tk-satz', neuerTab: true }), - h('p', { class: K.fehler, id: 'tk-error', hidden: true }), + h('p', { class: K.fehler, id: 'tk-error', role: 'alert', hidden: true }), h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, id: 'tk-submit' }, T.gutschrift.knopf || 'Antrag absenden'), h('button', { type: 'button', class: K.knopf, id: 'tk-cancel' }, 'Abbrechen'))) @@ -102,7 +102,7 @@ function antrag(r, viewAs) { h('form', { class: 'kn-tk-antwort', 'data-reply-for': r.id, novalidate: true }, h('textarea', { name: 'message', class: K.textarea, maxlength: 4000, placeholder: 'Deine Antwort…' }), h('input', { type: 'file', name: 'files', accept: 'application/pdf,image/jpeg,image/png', multiple: true, 'aria-label': 'Weitere Nachweise' }), - h('p', { class: K.fehler, hidden: true }), + h('p', { class: K.fehler, role: 'alert', hidden: true }), h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, disabled: viewAs }, 'Antwort senden')))] } else { koerper = [h('p', { class: 'kn-hinweis' }, 'Wir prüfen deine Angaben und antworten dir per E-Mail.')] diff --git a/public/neu/ansicht-speicher.js b/public/neu/ansicht-speicher.js index c5ff891..71a2929 100644 --- a/public/neu/ansicht-speicher.js +++ b/public/neu/ansicht-speicher.js @@ -75,7 +75,7 @@ function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) { summary = h('div', { class: 'kn-speicher-zusammenfassung kn-bestellen' }, baueBlock(zeilen, { id: 'storage-block' }), plus ? baueLinkzeile(T.speicher.linkzeile, { id: 'storage-linkzeile' }) : baueLinkzeile(T.speicher.warnung, { id: 'storage-warnung', klasse: 'kn-speicher-warnung' }), - h('p', { class: 'kn-fehler', id: 'storage-error', hidden: !fehlerText }, fehlerText), + h('p', { class: 'kn-fehler', id: 'storage-error', role: 'alert', hidden: !fehlerText }, fehlerText), h('div', { class: 'kn-aktionen' }, h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? (T.knopf || KNOPF) : 'Speichern')), plus ? baueKundenartZeile(kundenart, T.kundenart, { id: 'storage-kundenart' }) : null) diff --git a/public/neu/ansicht-wechsel.js b/public/neu/ansicht-wechsel.js index 7e3650c..d18f9e2 100644 --- a/public/neu/ansicht-wechsel.js +++ b/public/neu/ansicht-wechsel.js @@ -114,7 +114,7 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) { const zeichne = () => { const p = lauf.preview const uebersicht = lauf.error - ? h('p', { class: K.fehler }, lauf.error) + ? h('p', { class: K.fehler, role: 'alert' }, lauf.error) : lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Deine Übersicht wird berechnet …') : baueBlock(zeilenVon(p), { id: 'switch-block' }) const sperre = lauf.busy || viewAs @@ -131,7 +131,7 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) { baueLinkzeile(T.wechsel.linkzeile, { id: 'switch-linkzeile' }), haken ? h('label', { class: K.check }, haken, h('span', { id: 'switch-beginn-text' }, T.wechsel.haken)) : null, gesperrt ? h('p', { class: 'kn-sperrzeile', id: 'switch-sperre' }, T.wechsel.gesperrt) : null, - h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', hidden: !lauf.fehler }, lauf.fehler), + h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', role: 'alert', hidden: !lauf.fehler }, lauf.fehler), h('div', { class: 'kn-aktionen' }, h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)), viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null), diff --git a/public/neu/oeffentlich/kuendigen.html b/public/neu/oeffentlich/kuendigen.html index 43b2c51..0b4c478 100644 --- a/public/neu/oeffentlich/kuendigen.html +++ b/public/neu/oeffentlich/kuendigen.html @@ -55,7 +55,7 @@ - + diff --git a/public/neu/oeffentlich/login.html b/public/neu/oeffentlich/login.html index 248a0b5..154d208 100644 --- a/public/neu/oeffentlich/login.html +++ b/public/neu/oeffentlich/login.html @@ -54,7 +54,7 @@ autocomplete="current-password" enterkeyhint="go" > - + diff --git a/public/neu/vertraege-abo.js b/public/neu/vertraege-abo.js index 5b4f2cb..55b6dab 100644 --- a/public/neu/vertraege-abo.js +++ b/public/neu/vertraege-abo.js @@ -53,7 +53,7 @@ const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null, // "Zahlungsart aktualisieren": Stripe-Kundenportal; im Ansichtsmodus gesperrt function zahlungsKnopf(viewAs) { - const fehler = h('p', { class: K.fehler, hidden: true }) + const fehler = h('p', { class: K.fehler, role: 'alert', hidden: true }) const knopf = h('button', { type: 'button', class: K.primaer, disabled: viewAs }, 'Zahlungsart aktualisieren') knopf.addEventListener('click', async () => { if (viewAs) return diff --git a/test/knEfixCss.test.js b/test/knEfixCss.test.js index 419268c..09bdd5f 100644 --- a/test/knEfixCss.test.js +++ b/test/knEfixCss.test.js @@ -1,5 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import { lies, handyTeil } from './support/knHuelle.js' // Abschlussprüfung Phase E (Fix-Welle): Pins für die CSS-Befunde der neuen Seite. Sie lesen nur Quelltext, kein Browser. @@ -53,3 +54,24 @@ test('F3: Seitenränder der öffentlichen Seiten und des Chats berücksichtigen for (const sel of ['.kn-login-kopf', '.kn-login-haupt', '.kn-recht main', '.ab-seite .ab-innen']) assert.ok(hatInsets(oeff, sel), `oeffentlich.css: ${sel}`) assert.ok(hatInsets(lies('chat-composer.css'), '.chat-ov-top'), 'chat-composer.css: .chat-ov-top') }) + +// M3: Fehlermeldungen, die erst nach einer Aktion erscheinen, sind Live-Regionen (role="alert"), damit Screenreader sie vorlesen +const quelle = (datei) => readFileSync(new URL(`../public/neu/${datei}`, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') +test('M3: versteckte Fehlerzeilen der Bestellseiten, des Wechsels, des Speichers, der Gutschrift und der Zahlungsart tragen role="alert"', () => { + for (const datei of ['ansicht-bestellen.js', 'ansicht-wechsel.js', 'ansicht-speicher.js', 'ansicht-gutschrift.js', 'vertraege-abo.js']) { + const q = quelle(datei) + // jedes Attributobjekt eines Elements mit der Fehlerklasse, das versteckt starten kann (hidden), hat eine Rolle + const objekte = [...q.matchAll(/\{[^{}]*(?:K\.fehler|'kn-fehler'|fehlerKlasse)[^{}]*\}/g)].map((m) => m[0]).filter((o) => /\bhidden\b/.test(o)) + assert.ok(objekte.length > 0, `${datei}: keine versteckte Fehlerzeile gefunden`) + for (const o of objekte) assert.match(o, /role:\s*'alert'/, `${datei}: ${o}`) + } + // der Fehler der Wechsel-Vorschau steht nicht versteckt, wird aber nach dem Laden eingesetzt: auch er ist eine Live-Region + assert.match(quelle('ansicht-wechsel.js'), /h\('p', \{ class: K\.fehler, role: 'alert' \}, lauf\.error\)/) +}) +test('M3: die Fehlertexte der öffentlichen Seiten (Login, Kündigen, Widerrufen) sind Live-Regionen', () => { + for (const datei of ['login.html', 'kuendigen.html', 'widerrufen.html']) { + const html = readFileSync(new URL(`../public/neu/oeffentlich/${datei}`, import.meta.url), 'utf8') + const zeile = html.match(/]*id="(?:login|form)-error"[^>]*>/)?.[0] ?? '' + assert.match(zeile, /role="alert"/, `${datei}: ${zeile}`) + } +})