Terminplaner: Abschluss – Client (Formular-Token, Widerruf prominent, Telefonlink, Einbau-Code, G02)
- R53: Das Widget wartet vor jedem Absenden, bis das Formular-Token formMinSekunden plus 0,5 s alt
ist ("Einen Moment …", Knopf gesperrt); gilt nach dem Neuladen der Konfiguration und beim Wiederholen
nach "neu_laden". Kein stilles Erfolgs-201 ohne Buchung mehr.
- R54: "Vertrag widerrufen" im Fuß ist ein zweiter Knopf (44 px). Im Knopfmodus lädt das Widget die
Konfiguration beim Laden der Seite (ein Abruf) und zeigt bei Widerrufsrecht dauerhaft einen zweiten
Knopf "Vertrag widerrufen" neben dem Buchungsknopf, der den Dialog beim Widerrufsformular öffnet.
- Telefonlink im Widget und auf der Gästeseite: Text mit Buchstaben oder Mehrfachangaben wird nie zum
tel:-Link, "(0)" hinter der Landesvorwahl entfällt.
- Portal, Einstellungen: Kasten "Einbau auf deiner Website" mit Einbau-Code und Kopierknopf; Hinweistexte
zum Widerruf passen zu R55 (auch Anfragen).
- G02: Widerrufssatz nennt, dass der Knopf bis zum Beginn des Termins gilt und danach das Formular auf der
Website oder die E-Mail an den Betrieb.
- Einbau-Anleitung angepasst (siteKey und Code im Portal, Knopfmodus, Checkliste); Tests und Playwright.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd65c9dc58
commit
794a63c0d7
12 files changed
+536
-64
No files matched your search
@@ -1736,7 +1736,7 @@ async function pruefungen() {
|
||||
// Hinweise der Spezifikation
|
||||
const text = await textVon(page.locator('#tm-panel'))
|
||||
// Ruling R52: Wortlaut der rechtlichen Hinweise
|
||||
eq(await textVon(page.locator('#tm-panel .tm-box-info', { hasText: 'Privatpersonen können eine Buchung' })), 'Privatpersonen können eine Buchung innerhalb von 14 Tagen nach Vertragsschluss widerrufen (bei Anfragen zählt deine Annahme). Das gesetzliche Widerrufsrecht geht deiner Stornoregel vor: In dieser Zeit fällt bei einem Widerruf keine Stornogebühr an, auch wenn deine Stornofrist schon abgelaufen ist. Liegt der Termin innerhalb dieser 14 Tage, fragt der Terminplaner deine Gäste, ob du vorher beginnen darfst. Belehrung und Knopf „Vertrag widerrufen“ zeigt der Terminplaner automatisch, wenn deine Gäste Privatpersonen sind und du eine Leistung mit sofortiger Buchung anbietest.', 'Hinweis: Widerruf schlägt Stornoregel')
|
||||
eq(await textVon(page.locator('#tm-panel .tm-box-info', { hasText: 'Privatpersonen können eine Buchung' })), 'Privatpersonen können eine Buchung innerhalb von 14 Tagen nach Vertragsschluss widerrufen (bei Anfragen zählt deine Annahme). Das gesetzliche Widerrufsrecht geht deiner Stornoregel vor: In dieser Zeit fällt bei einem Widerruf keine Stornogebühr an, auch wenn deine Stornofrist schon abgelaufen ist. Liegt der Termin innerhalb dieser 14 Tage, fragt der Terminplaner deine Gäste, ob du vorher beginnen darfst. Belehrung und Knopf „Vertrag widerrufen“ zeigt der Terminplaner automatisch, wenn deine Gäste Privatpersonen sind, bei sofort buchbaren Leistungen ebenso wie bei Anfragen.', 'Hinweis: Widerruf schlägt Stornoregel (R55: auch Anfragen)')
|
||||
eq(await textVon(page.locator('#tm-panel label.tm-haken small', { hasText: 'Nur ankreuzen, wenn' })), 'Nur ankreuzen, wenn deine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB), zum Beispiel ein einzelner Tanzabend oder ein Escape Room. Friseur, Kosmetik, Massage, Tattoo, Beratung, Fotoshootings und Weiterbildungen sind in der Regel keine Freizeitleistung. Kreuzt du es an, zeigt der Terminplaner keine Widerrufsbelehrung. Kreuzt du es zu Unrecht an, können deine Gäste noch bis zu 12 Monate und 14 Tage widerrufen. Im Zweifel nicht ankreuzen oder dich rechtlich beraten lassen.', 'Hinweis: Freizeit')
|
||||
assert(text.includes('kein fertiger Rechtstext') && await page.locator('.tm-muster .tm-pille', { hasText: 'Muster' }).count() === 1, 'Stornotext-Muster als Muster gekennzeichnet')
|
||||
assert(text.includes('Bitte keine Gesundheitsangaben.'), 'Hinweis am Nachrichtenfeld')
|
||||
@@ -1860,6 +1860,43 @@ async function pruefungen() {
|
||||
}, { reset: { ohneEinstellung: true } })
|
||||
|
||||
// ------------------------------------------------------------ Abgeschaltet: alle neuen Reiter nur lesen
|
||||
// ------------------------------------------------------------ Einbau-Code der Website (Abschlussrunde C4)
|
||||
await check('echt_einbau_code_sichtbar_und_kopierbar', async (page, ctx) => {
|
||||
await ctx.grantPermissions(['clipboard-read', 'clipboard-write'], { origin: srv.url })
|
||||
await oeffneReiter(page, 'einstellungen')
|
||||
const box = page.locator('#tm-panel section', { has: page.getByRole('heading', { name: 'Einbau auf deiner Website' }) })
|
||||
await box.waitFor()
|
||||
const code = box.locator('textarea.tm-code-feld')
|
||||
const antwort = await page.evaluate(async () => (await (await fetch('/api/termine/einstellungen?projectId=p1')).json()).einbau.snippet)
|
||||
eq(await code.inputValue(), antwort, 'der Code ist der Einbau-Code des Servers')
|
||||
assert(antwort.includes(`data-webklar-termine="${KEY}"`) && antwort.includes('/embed/termine.js'), `Code mit siteKey und Skript: ${antwort}`)
|
||||
eq(await code.getAttribute('readonly'), '', 'nur lesbar')
|
||||
eq(await code.getAttribute('aria-label'), 'Einbau-Code für deine Website', 'Name des Feldes')
|
||||
const hoehe = await knopf(box, 'Code kopieren').boundingBox()
|
||||
assert(hoehe.height >= 44 || (await page.viewportSize()).width > 880, `Knopf gut treffbar: ${hoehe.height}`)
|
||||
await shot(page, 'einstellungen-einbau')
|
||||
await knopf(box, 'Code kopieren').click()
|
||||
await box.getByText('Der Code ist kopiert.').waitFor()
|
||||
eq(await page.evaluate(() => navigator.clipboard.readText()), antwort, 'Zwischenablage')
|
||||
assert(await passtInBreite(page), 'kein seitliches Scrollen')
|
||||
})
|
||||
|
||||
await check('echt_einbau_code_ohne_zwischenablage', async (page, ctx) => {
|
||||
await ctx.addInitScript(() => Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true })) // kein Zugriff auf die Zwischenablage
|
||||
await oeffneReiter(page, 'einstellungen')
|
||||
const box = page.locator('#tm-panel section', { has: page.getByRole('heading', { name: 'Einbau auf deiner Website' }) })
|
||||
await knopf(box, 'Code kopieren').click()
|
||||
const status = box.locator('[role="status"]')
|
||||
await status.filter({ hasText: 'Das Kopieren hat nicht geklappt.' }).waitFor()
|
||||
assert((await textVon(status)).includes('markiert'), 'Hinweis: Code ist markiert')
|
||||
const auswahl = await page.evaluate(() => {
|
||||
const el = document.activeElement
|
||||
return { code: el?.classList.contains('tm-code-feld'), von: el?.selectionStart, bis: el?.selectionEnd, laenge: el?.value.length }
|
||||
})
|
||||
eq(auswahl.code, true, 'Fokus im Code-Feld')
|
||||
eq([auswahl.von, auswahl.bis], [0, auswahl.laenge], 'der ganze Code ist markiert')
|
||||
})
|
||||
|
||||
await check('echt_abgeschaltet_reiter_nur_lesen', async (page) => {
|
||||
await srv.setzeFunktion('disabled')
|
||||
const v0 = version()
|
||||
@@ -1895,6 +1932,7 @@ async function pruefungen() {
|
||||
eq(await alleGesperrt(form), true, 'Einstellungen: alle Felder gesperrt')
|
||||
eq(await knopf(page, 'Einstellungen speichern').count(), 0, 'Einstellungen: kein Speichern')
|
||||
eq(await knopf(form, 'Muster in das Textfeld übernehmen').count() + await knopf(form, '+ Zusatzfeld').count(), 0, 'Einstellungen: keine Änderungsknöpfe')
|
||||
eq(await page.locator('#tm-panel [data-aktion="einbau-kopieren"], #tm-panel .tm-code-feld').count(), 0, 'Einstellungen: kein Einbau-Code bei abgeschalteter Funktion')
|
||||
eq(await page.getByRole('link', { name: /CSV/ }).count(), 1, 'Einstellungen: Export bleibt')
|
||||
assert((await textVon(page.locator('#tm-panel .tm-status'))).includes('Abgeschaltet'), 'Einstellungen: Status')
|
||||
await shot(page, 'abgeschaltet-einstellungen')
|
||||
|
||||
Reference in new issue
Block a user