Öffnungszeiten: Editor für Hinweis- und Kontakt-Leiste, Google und Vorschau
Hinweis-Leiste (Liste, „Hinweis-Leiste anlegen“, Art, Knopf, Zeitraum, Preis-Hinweis), Kontakt-Leiste (Telefon, Adresse, WhatsApp mit Datenschutz-Haken, R12 in der Kundenansicht), Google (Für Google kopieren, Profil-Link), Live-Vorschau mit Regler „Uhrzeit testen“ und UI-Prüfung test/ui/zeiten-portal.ui.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
242fbb01e5
commit
b3e96d4a56
8 files changed
+1684
-13
No files matched your search
@@ -0,0 +1,502 @@
|
||||
// Playwright-Checks der Portal-Ansicht Öffnungszeiten live (public/zeiten*.js, Kachel in funktionen.js, zeiten.css) – Aufgabe 30:
|
||||
// Kachel, Einschalten, leerer Editor, Speichern mit Konflikt, Hinweis-Vorschlag, Handy (390 px) und Tastatur. Nicht Teil von npm test.
|
||||
// Aufruf: free -m; nice node test/ui/zeiten-portal.ui.mjs (es läuft nur ein Browser)
|
||||
// Screenshots: /root/reports/zeiten-portal-ui/portal-*.png (bei Fehlern fehler-portal-<Prüfung>.png). Exit-Code 1, wenn eine Prüfung scheitert.
|
||||
//
|
||||
// Alle Prüfungen sind "fx_*": Fixture. Alle /api/-Anfragen beantwortet page.route mit festen Daten (wie test/ui/termine-portal.ui.mjs und
|
||||
// funktionen.ui.mjs); der Server liefert nur dashboard.html und die Dateien aus public/. Nichts verlässt den Rechner.
|
||||
// NUR=<Teil des Namens> führt nur passende Prüfungen aus (zum Entwickeln); STACK=1 zeigt bei Fehlern den Aufrufweg.
|
||||
// Uhr: Browser steht auf Montag 2026-10-05 10:00 Berliner Zeit (context.clock.setFixedTime).
|
||||
import http from 'node:http'
|
||||
import { mkdir, readdir, readFile, rm } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const PW = process.env.PLAYWRIGHT_CORE || '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
|
||||
const { chromium } = await import(PW)
|
||||
|
||||
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../public')
|
||||
const SHOTS = '/root/reports/zeiten-portal-ui'
|
||||
await mkdir(SHOTS, { recursive: true })
|
||||
for (const datei of await readdir(SHOTS)) if (/^(portal|fehler-portal)-.*\.png$/.test(datei)) await rm(path.join(SHOTS, datei)) // alte Läufe
|
||||
|
||||
const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.webmanifest': 'application/json' }
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://x')
|
||||
try {
|
||||
const datei = path.join(PUBLIC, url.pathname === '/' ? 'dashboard.html' : url.pathname)
|
||||
if (!datei.startsWith(PUBLIC)) throw new Error('außerhalb')
|
||||
const daten = await readFile(datei)
|
||||
res.writeHead(200, { 'content-type': TYPES[path.extname(datei)] || 'application/octet-stream' })
|
||||
res.end(daten)
|
||||
} catch {
|
||||
res.writeHead(404)
|
||||
res.end('nicht gefunden')
|
||||
}
|
||||
})
|
||||
await new Promise((fertig) => { server.listen(0, '127.0.0.1', fertig) })
|
||||
const BASE = `http://127.0.0.1:${server.address().port}`
|
||||
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin (MESZ)
|
||||
const HEUTE = '2026-10-05'
|
||||
|
||||
// ---------------------------------------------------------------- Helfer
|
||||
|
||||
function assert(cond, msg) { if (!cond) throw new Error(msg) }
|
||||
function eq(ist, soll, msg) {
|
||||
const a = JSON.stringify(ist)
|
||||
const b = JSON.stringify(soll)
|
||||
if (a !== b) throw new Error(`${msg}: ${a} statt ${b}`)
|
||||
}
|
||||
const tagPlus = (tag, n) => new Date(Date.parse(`${tag}T00:00:00Z`) + n * 86400000).toISOString().slice(0, 10)
|
||||
|
||||
/** Screenshot der ganzen Seite: das Portal scrollt im Dokument, fullPage zeigt dort nur die Kopfzeile; darum Fenster auf Seitenhöhe (höchstens 3600 px). */
|
||||
async function shot(page, name) {
|
||||
await page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.effect.getComputedTiming().iterations !== Infinity).map((a) => a.finished.catch(() => {}))))
|
||||
const fenster = page.viewportSize()
|
||||
const hoehe = await page.evaluate(() => Math.min(Math.max(document.documentElement.scrollHeight, window.innerHeight), 3600))
|
||||
await page.setViewportSize({ width: fenster.width, height: hoehe })
|
||||
await page.screenshot({ path: `${SHOTS}/portal-${name}.png` })
|
||||
await page.setViewportSize(fenster)
|
||||
}
|
||||
/** Kein Streifen rechts: die Seite passt in die Breite. */
|
||||
const passtInBreite = (page) => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
|
||||
// ---------------------------------------------------------------- Fixture
|
||||
|
||||
const WOCHENTAGE = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
|
||||
const woche = (zeiten = {}) => Object.fromEntries([1, 2, 3, 4, 5, 6, 7].map((n) => [n, zeiten[n] || { modus: 'zu', fenster: [] }]))
|
||||
const werktage = () => woche({
|
||||
1: { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] },
|
||||
2: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||||
3: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||||
4: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||||
5: { modus: 'zeiten', fenster: [['09:00', '16:00']] },
|
||||
6: { modus: 'zeiten', fenster: [['09:00', '12:00']] },
|
||||
})
|
||||
const PLAN = 'p_haupt001'
|
||||
const KONTAKT = { an: true, telefon: '496301123456', adresse: 'Hauptstraße 1, 67722 Winnweiler', whatsapp: { nummer: '4917112345678', starttext: 'Hallo, ich habe eine Frage zu euren Öffnungszeiten.', bestaetigtAt: '2026-10-01T08:00:00.000Z' } }
|
||||
const FEIERTAGE = [
|
||||
{ tag: '2026-11-01', name: 'Allerheiligen', art: 'feiertag' },
|
||||
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
|
||||
{ tag: '2026-12-25', name: 'Erster Weihnachtstag', art: 'feiertag' },
|
||||
{ tag: '2026-12-31', name: 'Silvester', art: 'pruefen' },
|
||||
]
|
||||
const gespeicherteDaten = (extra = {}) => ({
|
||||
land: 'RP',
|
||||
plaene: [{ id: PLAN, kurz: 'oeffnungszeiten', name: 'Öffnungszeiten', haupt: true, tage: werktage() }],
|
||||
feiertage: {},
|
||||
ausnahmen: [],
|
||||
hinweise: [],
|
||||
kontakt: KONTAKT,
|
||||
...extra,
|
||||
})
|
||||
const googleTextAus = (daten) => {
|
||||
const tage = daten.plaene[0].tage
|
||||
const text = (t) => (t.modus === 'zeiten' ? t.fenster.map((f) => `${f[0]}–${f[1]}`).join(', ') : t.modus === 'rund' ? 'rund um die Uhr' : t.modus === 'vereinbarung' ? 'nach Vereinbarung' : 'geschlossen')
|
||||
return WOCHENTAGE.map((name, i) => `${name}: ${text(tage[i + 1])}`).join('\n')
|
||||
}
|
||||
|
||||
const FX_CONSENT = { version: 'fx-1', knopf: 'Einschalten', items: [{ id: 'leistung', text: 'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen.', haken: true }] }
|
||||
|
||||
/** Fixture: Daten, die page.route statt der Schnittstellen /api/funktionen und /api/zeiten liefert. */
|
||||
function neueFixture(o = {}) {
|
||||
const fx = {
|
||||
state: 'active',
|
||||
inklusive: true,
|
||||
grund: null,
|
||||
zeitenLeer: false,
|
||||
daten: gespeicherteDaten(),
|
||||
version: 3,
|
||||
konflikt: false, // das nächste PUT antwortet 409 (jemand hat inzwischen gespeichert)
|
||||
aufrufe: [],
|
||||
...o,
|
||||
}
|
||||
fx.angebot = () => ({
|
||||
verfuegbar: true,
|
||||
inklusive: fx.inklusive,
|
||||
grund: fx.grund,
|
||||
preis: '30 €',
|
||||
einschalten: { consent: FX_CONSENT, leistungsbeschreibung: { version: 'fx-1', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } },
|
||||
belehrung: 'Widerrufsbelehrung der Fixture.',
|
||||
})
|
||||
fx.feature = () => ({
|
||||
state: fx.state, siteKey: 'abcdabcdabcdabcd', ticketWoid: fx.state === 'off' ? null : '1234', orderedAt: null, activatedAt: null, invoiceUrl: null,
|
||||
disabledReason: null, disabledAt: null, loeschungAm: null, unlockedBy: fx.state === 'off' ? null : 'abo', aboEndeAt: null, einbau: fx.state === 'setup' ? 'angestossen' : null,
|
||||
})
|
||||
fx.vorschau = () => (fx.daten ? { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' } : null)
|
||||
fx.liste = () => ({
|
||||
projects: [{
|
||||
projectId: 'p1', projectName: 'Bäckerei Sonnenschein', liveUrl: 'https://baeckerei.example', features: { news: { state: 'off' }, zeiten: fx.feature() },
|
||||
zeitenStatus: fx.state === 'active' && fx.daten ? fx.vorschau() : null, zeitenLeer: fx.zeitenLeer,
|
||||
}],
|
||||
features: { zeiten: { consent: FX_CONSENT, priceLabel: '30 €', setupEstimate: 'meist innerhalb von 1–2 Wochen', leistungsbeschreibung: { version: 'fx-1', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } } },
|
||||
ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG',
|
||||
zeiten: fx.angebot(),
|
||||
})
|
||||
fx.ansicht = () => ({
|
||||
state: fx.state, daten: fx.daten, version: fx.version, kundenansicht: false, vorschau: fx.vorschau(), feiertage: FEIERTAGE, googleText: fx.daten ? googleTextAus(fx.daten) : '',
|
||||
})
|
||||
return fx
|
||||
}
|
||||
|
||||
/** Die übrigen Portal-Schnittstellen, die dashboard.html beim Laden braucht (wie test/ui/funktionen.ui.mjs). */
|
||||
function portalHilfe(p, json) {
|
||||
if (p === '/api/auth/me') return json({ authenticated: true, role: 'customer', customer: { id: 'c1', code: '1001', name: 'Berta Berg', email: 'berta@baeckerei.example' } })
|
||||
if (p === '/api/projects') return json({ projects: [{ id: 'p1', name: 'Bäckerei Sonnenschein', status: 'live' }] })
|
||||
if (p === '/api/features') return json({ features: [] })
|
||||
if (p === '/api/contact') return json({ name: 'WEBklar-Team', responseTime: 'Antwort meist < 1 Werktag' })
|
||||
if (p === '/api/chat/unread-count') return json({ unread: 0 })
|
||||
return json({})
|
||||
}
|
||||
|
||||
/** Antworten der Fixture auf /api/funktionen und /api/zeiten (Teil von page.route). */
|
||||
function fixtureAntwort(fx, req, url, json) {
|
||||
const p = url.pathname
|
||||
const m = req.method()
|
||||
if (p === '/api/funktionen') return json(fx.liste())
|
||||
if (p === '/api/funktionen/zeiten/einschalten' && m === 'POST') {
|
||||
fx.aufrufe.push(['einschalten', req.postDataJSON()])
|
||||
fx.state = 'setup'
|
||||
fx.zeitenLeer = true
|
||||
fx.daten = null
|
||||
fx.version = 0
|
||||
return json({ feature: { state: 'setup' }, einbau: 'angestossen' })
|
||||
}
|
||||
if (p === '/api/zeiten/p1' && m === 'GET') {
|
||||
fx.aufrufe.push(['get'])
|
||||
return json(fx.ansicht())
|
||||
}
|
||||
if (p === '/api/zeiten/p1' && m === 'PUT') {
|
||||
const body = req.postDataJSON()
|
||||
fx.aufrufe.push(['put', body])
|
||||
if (fx.konflikt) {
|
||||
// jemand anders hat inzwischen gespeichert: neuer Stand, neue Version
|
||||
fx.konflikt = false
|
||||
fx.version += 1
|
||||
fx.daten = { ...fx.daten, plaene: [{ ...fx.daten.plaene[0], name: 'Öffnungszeiten (inzwischen geändert)' }] }
|
||||
return json({ error: 'Inzwischen geändert – bitte neu laden.', code: 'ZEITEN_VERALTET' }, 409)
|
||||
}
|
||||
if (body.version !== fx.version) return json({ error: 'Inzwischen geändert – bitte neu laden.', code: 'ZEITEN_VERALTET' }, 409)
|
||||
const erstes = fx.daten === null
|
||||
fx.daten = { ...body.daten, kontakt: body.daten.kontakt || { an: false, telefon: '', adresse: '', whatsapp: null } }
|
||||
fx.version += 1
|
||||
if (erstes) fx.zeitenLeer = false
|
||||
return json(fx.ansicht())
|
||||
}
|
||||
if (p === '/api/zeiten/p1/vorschau' && m === 'GET') {
|
||||
const zeit = url.searchParams.get('zeit') || ''
|
||||
fx.aufrufe.push(['vorschau', zeit])
|
||||
if (!fx.daten) return json({ error: 'Bitte speichere zuerst deine Öffnungszeiten.', code: 'keine_zeiten' }, 409)
|
||||
const wann = new Date(`${zeit}:00Z`)
|
||||
const stunde = wann.getUTCHours()
|
||||
const sonntag = wann.getUTCDay() === 0
|
||||
if (sonntag) return json({ offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' })
|
||||
return json(stunde >= 9 && stunde < 18 ? { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' } : { offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' })
|
||||
}
|
||||
return json({})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ablauf der Prüfungen
|
||||
|
||||
const results = []
|
||||
let browser = null
|
||||
const hosts = new Set()
|
||||
|
||||
/** Eine Prüfung in eigenem Browser-Kontext. optionen: viewport/context (Playwright), fixture (Objekt aus neueFixture()). */
|
||||
async function check(name, fn, { context = {}, fixture = neueFixture() } = {}) {
|
||||
if (process.env.NUR && !name.includes(process.env.NUR)) return
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: 'de-DE', timezoneId: 'Europe/Berlin', ...context })
|
||||
await ctx.clock.setFixedTime(JETZT)
|
||||
ctx.setDefaultTimeout(8000)
|
||||
const fehler = []
|
||||
ctx.on('page', (seite) => {
|
||||
seite.on('pageerror', (e) => fehler.push(`JavaScript-Fehler: ${e.message}`))
|
||||
seite.on('dialog', (d) => { fehler.push(`Browser-Dialog ${d.type()}: ${d.message()}`); d.dismiss().catch(() => {}) })
|
||||
seite.on('request', (r) => { try { const u = new URL(r.url()); if (u.protocol.startsWith('http')) hosts.add(u.host) } catch { /* data: u. a. */ } })
|
||||
})
|
||||
await ctx.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = new URL(req.url())
|
||||
const json = (body, status = 200) => route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) })
|
||||
if (url.pathname.startsWith('/api/funktionen') || url.pathname.startsWith('/api/zeiten')) return fixtureAntwort(fixture, req, url, json)
|
||||
return portalHilfe(url.pathname, json)
|
||||
})
|
||||
const page = await ctx.newPage()
|
||||
page.fx = fixture
|
||||
try {
|
||||
await fn(page, ctx, fixture)
|
||||
assert(fehler.length === 0, fehler.join(' | '))
|
||||
results.push([name, true])
|
||||
console.log(`ok ${name}`)
|
||||
} catch (e) {
|
||||
results.push([name, false])
|
||||
console.log(`FAIL ${name}: ${e.message.split('\n')[0]}`)
|
||||
if (process.env.STACK) console.log(e.stack)
|
||||
await page.screenshot({ path: `${SHOTS}/fehler-portal-${name}.png` }).catch(() => {})
|
||||
} finally {
|
||||
await ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
const oeffne = async (page, hash) => {
|
||||
await page.goto(`${BASE}/dashboard.html${hash}`)
|
||||
await page.waitForFunction(() => document.getElementById('customer-name')?.textContent)
|
||||
}
|
||||
/** Ansicht #zeiten öffnen und warten, bis der Editor (mit Abschnitten) da ist. */
|
||||
const oeffneEditor = async (page) => {
|
||||
await oeffne(page, '#zeiten')
|
||||
await page.locator('#view-zeiten.active').waitFor()
|
||||
await page.locator('#zeiten-body .zt-editor [data-abschnitt="plaene"]').waitFor()
|
||||
}
|
||||
const editor = (page) => page.locator('#zeiten-body .zt-editor')
|
||||
const abschnitt = (page, key) => page.locator(`#zeiten-body [data-abschnitt="${key}"]`)
|
||||
const knopf = (loc, name) => loc.getByRole('button', { name }) // Teiltreffer: wie die Relaunch-Station („Hinweis-Leiste anlegen“)
|
||||
const meldung = (page) => page.locator('#zeiten-body [data-meldung="editor"]')
|
||||
|
||||
async function pruefungen() {
|
||||
// ---------------------------------------------------------------- Kachel in Funktionen
|
||||
await check('fx_kachel_zustaende', async (page, _ctx, fx) => {
|
||||
const faelle = [
|
||||
['off, im Abo enthalten', { state: 'off', inklusive: true, zeitenLeer: true, daten: null }, 'Aus', /Im Wartungs-Abo enthalten\./, 'Einschalten'],
|
||||
['off, 30 €', { state: 'off', inklusive: false, grund: 'kein_abo', zeitenLeer: true, daten: null }, 'Aus', /30 € einmalig/, 'Öffnungszeiten einrichten · 30 €'],
|
||||
['off, Abo noch nicht aktiv', { state: 'off', inklusive: false, grund: 'nicht_aktiv', zeitenLeer: true, daten: null }, 'Aus', /einschaltbar, sobald dein Abo aktiv ist/, null],
|
||||
['off, nicht prüfbar', { state: 'off', inklusive: null, grund: null, zeitenLeer: true, daten: null }, 'Aus', /Gerade nicht prüfbar – bitte später erneut\./, null],
|
||||
['Zahlung offen', { state: 'ordered', inklusive: false, grund: 'kein_abo', zeitenLeer: true, daten: null }, 'Zahlung offen', /Zahlung wird bestätigt/, 'Status ansehen'],
|
||||
['In Einrichtung, leer', { state: 'setup', zeitenLeer: true, daten: null }, 'In Einrichtung', /Trag jetzt deine Zeiten ein/, 'Zeiten bearbeiten'],
|
||||
['Aktiv mit Status', { state: 'active' }, 'Aktiv', /Jetzt geöffnet · schließt um 18 Uhr/, 'Zeiten bearbeiten'],
|
||||
['Abgeschaltet', { state: 'disabled', zeitenLeer: true, daten: null }, 'Abgeschaltet', /abgeschaltet/, 'Chat öffnen'],
|
||||
]
|
||||
for (const [name, zustand, pille, text, cta] of faelle) {
|
||||
Object.assign(fx, { state: 'active', inklusive: true, grund: null, zeitenLeer: false, daten: gespeicherteDaten() }, zustand)
|
||||
await page.goto('about:blank')
|
||||
await oeffne(page, '#funktionen')
|
||||
const kachel = page.locator('[data-fn-tile="zeiten"]')
|
||||
await kachel.waitFor()
|
||||
eq((await kachel.locator('.fn-pill').innerText()).trim(), pille, `${name}: Pille`)
|
||||
assert(text.test(await kachel.innerText()), `${name}: Text fehlt: ${text} in „${(await kachel.innerText()).replace(/\n/g, ' | ')}“`)
|
||||
if (cta) eq((await kachel.locator('.fn-tile-cta').first().innerText()).trim(), cta, `${name}: Knopf`)
|
||||
else eq(await kachel.locator('.fn-tile-cta').count(), 0, `${name}: kein Knopf`)
|
||||
assert(await passtInBreite(page), `${name}: waagerechter Streifen`)
|
||||
}
|
||||
await shot(page, 'kachel-abgeschaltet')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Einschalten
|
||||
await check('fx_einschalten', async (page, _ctx, fx) => {
|
||||
await oeffne(page, '#funktionen')
|
||||
await knopf(page.locator('[data-fn-tile="zeiten"]'), 'Einschalten').click()
|
||||
const seite = page.locator('[data-variante="einschalten"]')
|
||||
await seite.waitFor()
|
||||
const einschalten = knopf(seite, 'Einschalten')
|
||||
assert(await einschalten.isDisabled(), 'ohne Haken gesperrt')
|
||||
await shot(page, 'einschalten')
|
||||
await seite.locator('input[type="checkbox"]').check()
|
||||
assert(await einschalten.isEnabled(), 'der Haken gibt den Knopf frei')
|
||||
await einschalten.click()
|
||||
await page.locator('.zt-status[data-state="setup"]').waitFor()
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'einschalten'), [['einschalten', { projectId: 'p1', zustimmung: { version: 'fx-1', haken: ['leistung'] } }]], 'POST Einschalten')
|
||||
assert((await page.locator('.zt-status [data-danke]').innerText()).includes('Öffnungszeiten live ist eingeschaltet.'), 'Danke-Satz')
|
||||
await page.locator('#zeiten-body .zt-editor [data-hinweis="leer"]').waitFor()
|
||||
assert((await page.locator('#zeiten-body .zt-editor [data-hinweis="leer"]').innerText()).includes('Trag deine Öffnungszeiten ein'), 'der Editor wartet auf die ersten Zeiten')
|
||||
eq(await page.locator('#zeiten-body details[data-plan]').count(), 1, 'ein leerer Hauptplan')
|
||||
assert(await passtInBreite(page), 'waagerechter Streifen')
|
||||
await shot(page, 'einschalten-danach')
|
||||
}, { fixture: neueFixture({ state: 'off', inklusive: true, zeitenLeer: true, daten: null, version: 0 }) })
|
||||
|
||||
// ---------------------------------------------------------------- Leerer Editor, erstes Speichern
|
||||
await check('fx_editor_leer', async (page, _ctx, fx) => {
|
||||
await oeffneEditor(page)
|
||||
assert((await page.locator('.zt-status').innerText()).includes('Trag jetzt deine Zeiten ein'), 'Statuskarte fordert die Zeiten')
|
||||
assert((await editor(page).innerText()).includes('Trag deine Öffnungszeiten ein'), 'Editor: Trag deine Öffnungszeiten ein')
|
||||
eq(await page.locator('#zeiten-body details[data-plan]').count(), 1, 'ein leerer Hauptplan')
|
||||
assert((await page.locator('#zeiten-body [data-kurz]').innerText()).trim() === 'oeffnungszeiten', 'Kurzname oeffnungszeiten')
|
||||
for (const key of ['feiertage', 'ausnahmen', 'hinweise', 'kontakt', 'google']) eq(await abschnitt(page, key).count(), 0, `${key}: erst nach dem ersten Speichern`)
|
||||
assert((await page.locator('[data-vorschau="jetzt"]').innerText()).toLowerCase().includes('noch keine öffnungszeiten gespeichert'), 'Vorschau: noch nichts gespeichert')
|
||||
// Regler vor dem ersten Speichern: ein Hinweis, kein Fehler
|
||||
await page.locator('[data-regler="zeit"]').fill('11:00')
|
||||
await page.locator('[data-vorschau="test"]', { hasText: 'Speichere zuerst deine Öffnungszeiten' }).waitFor()
|
||||
eq(await page.locator('[data-vorschau="test"]').getAttribute('role'), null, 'der Hinweis ist kein Alarm')
|
||||
await shot(page, 'editor-leer')
|
||||
// Montag eintragen und speichern
|
||||
const montag = page.locator('details[data-plan] [data-tag="1"]')
|
||||
await montag.locator('input[type="checkbox"]').check()
|
||||
const zeiten = montag.locator('input[type="time"]')
|
||||
await zeiten.nth(0).fill('09:00')
|
||||
await zeiten.nth(1).fill('12:30')
|
||||
await zeiten.nth(2).fill('13:30')
|
||||
await zeiten.nth(3).fill('18:00')
|
||||
await knopf(page.locator('details[data-plan]'), 'Speichern').click()
|
||||
await abschnitt(page, 'feiertage').waitFor()
|
||||
for (const key of ['ausnahmen', 'hinweise', 'kontakt', 'google']) await abschnitt(page, key).waitFor()
|
||||
const put = fx.aufrufe.find((a) => a[0] === 'put')[1]
|
||||
eq(put.version, 0, 'erstes PUT mit version 0')
|
||||
eq(put.daten.plaene[0].tage['1'], { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] }, 'Montag gespeichert')
|
||||
assert(!(await page.locator('.zt-status').innerText()).includes('Trag jetzt deine Zeiten ein'), 'der Kopf kennt die Zeiten jetzt')
|
||||
assert((await page.locator('[data-vorschau="jetzt"]').innerText()).includes('Jetzt geöffnet'), 'Vorschau aus der Antwort')
|
||||
await shot(page, 'editor-gespeichert')
|
||||
}, { fixture: neueFixture({ state: 'active', zeitenLeer: true, daten: null, version: 0 }) })
|
||||
|
||||
// ---------------------------------------------------------------- Speichern mit Konflikt
|
||||
await check('fx_speichern_konflikt', async (page, _ctx, fx) => {
|
||||
await oeffneEditor(page)
|
||||
fx.konflikt = true
|
||||
const name = page.locator('details[data-plan] input[type="text"]').first()
|
||||
await name.fill('Öffnungszeiten neu')
|
||||
await knopf(page.locator('details[data-plan]'), 'Speichern').click()
|
||||
const box = page.locator('#zeiten-body .zt-editor [role="alert"]', { hasText: 'Inzwischen geändert – bitte neu laden.' })
|
||||
await box.waitFor()
|
||||
assert(await knopf(box, 'Neu laden').isVisible(), 'Knopf „Neu laden“')
|
||||
await shot(page, 'konflikt')
|
||||
await knopf(box, 'Neu laden').click()
|
||||
await page.waitForFunction(() => !document.querySelector('#zeiten-body .zt-editor [role="alert"]:not([hidden])') || [...document.querySelectorAll('#zeiten-body .zt-editor .tm-box[role="alert"]')].every((e) => e.hidden))
|
||||
eq(await page.locator('details[data-plan] input[type="text"]').first().inputValue(), 'Öffnungszeiten (inzwischen geändert)', 'der Stand vom Server steht da')
|
||||
// jetzt geht Speichern mit der neuen Version
|
||||
await knopf(page.locator('details[data-plan]'), 'Speichern').click()
|
||||
await meldung(page).filter({ hasText: 'gespeichert' }).waitFor()
|
||||
const puts = fx.aufrufe.filter((a) => a[0] === 'put').map((a) => a[1].version)
|
||||
eq(puts, [3, 4], 'zweites PUT mit der neuen Version')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Urlaub ⇒ Hinweis-Vorschlag, „Hinweis-Leiste anlegen“
|
||||
await check('fx_hinweis_vorschlag', async (page, _ctx, fx) => {
|
||||
await oeffneEditor(page)
|
||||
const von = tagPlus(HEUTE, 20)
|
||||
const bis = tagPlus(HEUTE, 25)
|
||||
await abschnitt(page, 'ausnahmen').getByRole('button', { name: '+ Betriebsurlaub eintragen' }).click()
|
||||
const form = page.getByRole('form', { name: 'Betriebsurlaub eintragen' })
|
||||
await form.locator('input[type="date"]').nth(0).fill(von)
|
||||
await form.locator('input[type="date"]').nth(1).fill(bis)
|
||||
await form.locator('input[type="text"]').fill('Sommerpause')
|
||||
await form.getByRole('button', { name: 'Betriebsurlaub eintragen' }).click()
|
||||
const frage = page.getByRole('form', { name: 'Hinweis-Leiste dazu anlegen?' })
|
||||
await frage.waitFor()
|
||||
const kurz = (t) => `${t.slice(8)}.${t.slice(5, 7)}.`
|
||||
assert((await frage.innerText()).includes(`Betriebsurlaub vom ${kurz(von)} bis ${kurz(bis)}`), 'Vorschlag mit Zeitraum')
|
||||
await shot(page, 'hinweis-vorschlag')
|
||||
await knopf(frage, 'Ja, anlegen').click()
|
||||
const liste = abschnitt(page, 'hinweise')
|
||||
await liste.locator('li', { hasText: `Betriebsurlaub vom ${kurz(von)} bis ${kurz(bis)}` }).waitFor()
|
||||
const hinweise = fx.aufrufe.filter((a) => a[0] === 'put').at(-1)[1].daten.hinweise
|
||||
eq(hinweise.map((h) => [h.art, h.knopf, h.von, h.bis]), [['urlaub', null, tagPlus(von, -7), bis]], 'Hinweis im zweiten PUT')
|
||||
|
||||
// „Hinweis-Leiste anlegen“: der Knopf, den die Relaunch-Station antippt
|
||||
const anlegen = page.getByRole('button', { name: 'Hinweis-Leiste anlegen', exact: true })
|
||||
assert(await anlegen.isVisible(), 'Knopf „Hinweis-Leiste anlegen“ ist sichtbar')
|
||||
await anlegen.click()
|
||||
const neu = page.getByRole('form', { name: 'Neuer Hinweis' })
|
||||
await neu.waitFor()
|
||||
await neu.locator('input[type="text"]').first().fill('Heute 20 % auf alle Torten')
|
||||
await neu.locator('[data-hinweis="preis"]').waitFor({ state: 'visible' })
|
||||
await shot(page, 'hinweis-formular')
|
||||
await neu.locator('select').first().selectOption('aktion')
|
||||
await knopf(neu, 'Hinweis speichern').click()
|
||||
await neu.locator('[data-fehler$=".bis"]', { hasText: 'Eine Aktion braucht ein Enddatum.' }).waitFor()
|
||||
await neu.locator('input[type="date"]').nth(1).fill(tagPlus(HEUTE, 3))
|
||||
await knopf(neu, 'Hinweis speichern').click()
|
||||
await liste.locator('li', { hasText: 'Heute 20 % auf alle Torten' }).waitFor()
|
||||
eq(fx.aufrufe.filter((a) => a[0] === 'put').length, 3, 'drei Speichervorgänge')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Handy 390 px
|
||||
await check('fx_handy_390', async (page, _ctx, fx) => {
|
||||
fx.daten = gespeicherteDaten({
|
||||
hinweise: [{ id: 'h_aktion001', art: 'aktion', text: '20 % auf alle Torten – nur diese Woche, solange der Vorrat reicht, bei Abholung im Laden', knopf: { art: 'link', ziel: 'https://baeckerei.example/torten/aktion/herbst', text: 'Mehr erfahren' }, von: HEUTE, bis: tagPlus(HEUTE, 4) }],
|
||||
})
|
||||
await oeffneEditor(page)
|
||||
await page.locator('details[data-plan] summary').first().scrollIntoViewIfNeeded()
|
||||
// alles aufklappen, was aufklappt: Hinweis-Formular, WhatsApp-Felder, Regler
|
||||
await abschnitt(page, 'hinweise').locator('li[data-id="h_aktion001"] [data-aktion="bearbeiten"]').click()
|
||||
await abschnitt(page, 'ausnahmen').getByRole('button', { name: '+ Betriebsurlaub eintragen' }).click()
|
||||
await page.locator('[data-regler="zeit"]').fill('19:30')
|
||||
await page.locator('[data-vorschau="test"]', { hasText: 'Geschlossen' }).waitFor()
|
||||
assert(await passtInBreite(page), 'waagerechter Streifen')
|
||||
// nichts im Editor ragt über den rechten Rand hinaus
|
||||
const ueberstehend = await page.evaluate(() => {
|
||||
const breite = window.innerWidth
|
||||
return [...document.querySelectorAll('#zeiten-body *')].filter((el) => {
|
||||
const r = el.getBoundingClientRect()
|
||||
return r.width > 0 && r.height > 0 && (r.right > breite + 0.5 || r.left < -0.5)
|
||||
}).slice(0, 5).map((el) => `${el.tagName.toLowerCase()}.${el.className}`)
|
||||
})
|
||||
eq(ueberstehend, [], 'nichts ragt über den Rand')
|
||||
// Tippflächen mindestens 44 px (Knöpfe, Auswahllisten, Eingabefelder; Haken über ihre Beschriftungszeile)
|
||||
const zuKlein = await page.evaluate(() => [...document.querySelectorAll('#zeiten-body button, #zeiten-body select, #zeiten-body input:not([type="checkbox"]), #zeiten-body summary, #zeiten-body a[href]')]
|
||||
.filter((el) => el.getClientRects().length && !el.matches(':disabled'))
|
||||
.map((el) => ({ el, h: el.getBoundingClientRect().height }))
|
||||
.filter(({ h }) => h < 43.5)
|
||||
.slice(0, 6)
|
||||
.map(({ el, h }) => `${el.tagName.toLowerCase()}[${el.getAttribute('data-aktion') || el.getAttribute('data-feld') || el.className}] ${Math.round(h)}px`))
|
||||
eq(zuKlein, [], 'Tippflächen ≥ 44 px')
|
||||
await shot(page, 'handy-editor')
|
||||
// Kontakt und Google unten
|
||||
await abschnitt(page, 'kontakt').scrollIntoViewIfNeeded()
|
||||
await shot(page, 'handy-kontakt')
|
||||
assert(await passtInBreite(page), 'waagerechter Streifen (unten)')
|
||||
}, { context: { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true } })
|
||||
|
||||
// ---------------------------------------------------------------- Tastatur
|
||||
await check('fx_tastatur', async (page, _ctx, fx) => {
|
||||
fx.daten = gespeicherteDaten({
|
||||
hinweise: [{ id: 'h_aktion001', art: 'aktion', text: '20 % auf alle Torten', knopf: { art: 'link', ziel: 'https://baeckerei.example/torten', text: 'Mehr erfahren' }, von: HEUTE, bis: tagPlus(HEUTE, 4) }],
|
||||
})
|
||||
await oeffneEditor(page)
|
||||
await abschnitt(page, 'hinweise').getByRole('button', { name: 'Hinweis-Leiste anlegen' }).click()
|
||||
await abschnitt(page, 'ausnahmen').getByRole('button', { name: '+ Sondertag eintragen' }).click()
|
||||
await page.locator('details[data-plan] summary').first().scrollIntoViewIfNeeded()
|
||||
// alles, was sichtbar und bedienbar ist, bekommt eine Nummer
|
||||
const erwartet = await page.evaluate(() => {
|
||||
const fokus = 'a[href], button, input:not([type="hidden"]), select, textarea, summary, [tabindex]:not([tabindex="-1"])'
|
||||
const sichtbar = (el) => !el.matches(':disabled') && el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden'
|
||||
const liste = [...document.querySelectorAll(`#zeiten-body ${fokus.split(', ').join(', #zeiten-body ')}`)].filter(sichtbar)
|
||||
liste.forEach((el, i) => el.setAttribute('data-tabidx', String(i)))
|
||||
liste[0].focus()
|
||||
return liste.map((el) => `${el.tagName.toLowerCase()}:${el.getAttribute('data-aktion') || el.getAttribute('data-feld') || el.getAttribute('aria-label') || el.id || el.className}`)
|
||||
})
|
||||
assert(erwartet.length > 40, `genug Bedienelemente (${erwartet.length})`)
|
||||
// Zeit- und Datumsfelder haben in Chromium mehrere Tab-Stopps (Stunde, Minute, Tag …): bleibt der Fokus im selben Element, zählt es nicht als nächstes
|
||||
const erreicht = new Set([0])
|
||||
const ohneFokusring = []
|
||||
let aktuell = 0
|
||||
for (let versuch = 0; aktuell < erwartet.length - 1; versuch += 1) {
|
||||
if (versuch > erwartet.length * 8) throw new Error(`Tab: nach ${versuch} Tastendrücken erst bei ${aktuell + 1} von ${erwartet.length}`)
|
||||
await page.keyboard.press('Tab')
|
||||
const stand = await page.evaluate(() => {
|
||||
const el = document.activeElement
|
||||
const stil = el ? getComputedStyle(el) : null
|
||||
return el && el.hasAttribute('data-tabidx')
|
||||
? { idx: Number(el.getAttribute('data-tabidx')), ring: stil.outlineStyle !== 'none' && parseFloat(stil.outlineWidth) > 0, schatten: stil.boxShadow !== 'none' }
|
||||
: { idx: -1 }
|
||||
})
|
||||
if (stand.idx < 0) throw new Error(`Tab: Fokus außerhalb des Editors (erwartet: ${erwartet[aktuell + 1]})`)
|
||||
if (stand.idx === aktuell) continue
|
||||
if (stand.idx !== aktuell + 1) throw new Error(`Tab: Fokus auf ${erwartet[stand.idx]} statt ${erwartet[aktuell + 1]} (Reihenfolge oder übersprungen)`)
|
||||
aktuell = stand.idx
|
||||
erreicht.add(aktuell)
|
||||
if (!stand.ring && !stand.schatten) ohneFokusring.push(erwartet[aktuell])
|
||||
}
|
||||
eq([...erreicht].length, erwartet.length, 'alle Felder per Tab erreichbar')
|
||||
eq(ohneFokusring, [], 'Fokus überall sichtbar')
|
||||
await shot(page, 'tastatur-ende')
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lauf
|
||||
|
||||
let abbruch = null
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
await pruefungen()
|
||||
const fremd = [...hosts].filter((h) => h !== new URL(BASE).host)
|
||||
results.push(['nur_eigene_herkunft', fremd.length === 0])
|
||||
console.log(fremd.length ? `FAIL nur_eigene_herkunft: Anfragen an fremde Hosts: ${fremd.join(', ')}` : 'ok nur_eigene_herkunft')
|
||||
} catch (e) {
|
||||
abbruch = e
|
||||
console.log(`ABBRUCH: ${e.stack || e.message}`)
|
||||
} finally {
|
||||
if (browser) await browser.close().catch(() => {})
|
||||
server.close()
|
||||
}
|
||||
|
||||
const fehlgeschlagen = results.filter(([, ok]) => !ok)
|
||||
console.log(`${results.length - fehlgeschlagen.length}/${results.length} Checks bestanden`)
|
||||
process.exit(abbruch || fehlgeschlagen.length ? 1 : 0)
|
||||
+654
-4
@@ -13,8 +13,9 @@ import * as editor from '../public/zeiten-editor.js'
|
||||
import * as speichern from '../public/zeiten-speichern.js'
|
||||
|
||||
// Editor der Öffnungszeiten live, Teil I (Task 29): Speichern, Zeitpläne, Feiertage, Ausnahmen (public/zeiten-editor.js, zeiten-speichern.js,
|
||||
// zeiten-plaene.js, zeiten-ausnahmen.js; Spec 3.4 Bereiche 1 bis 3 und der Absatz „Speichern“). Fake-DOM und Fake-fetch aus
|
||||
// test/support/fakeDom.js: jeder Aufruf steht in fx.fetchLog, die Antworten kommen der Reihe nach aus fx.antworten.
|
||||
// zeiten-plaene.js, zeiten-ausnahmen.js; Spec 3.4 Bereiche 1 bis 3 und der Absatz „Speichern“).
|
||||
// Teil II (Task 30): Hinweis-Leiste, Kontakt-Leiste, Google und Vorschau (zeiten-leisten.js, zeiten-google.js, zeiten-editor.js).
|
||||
// Fake-DOM und Fake-fetch aus test/support/fakeDom.js: jeder Aufruf steht in fx.fetchLog, die Antworten kommen der Reihe nach aus fx.antworten.
|
||||
|
||||
const mitDom = (optionen, fn) => async () => {
|
||||
const fx = installFakeDom(optionen)
|
||||
@@ -1196,11 +1197,660 @@ test('Hülle: eine andere Website oder ein anderer Schreibstatus (abgeschaltet)
|
||||
assert.deepEqual(gets(fx).filter((a) => a.url.startsWith('/api/zeiten/')).map((a) => a.url), ['/api/zeiten/p1', '/api/zeiten/p2'])
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Editor II (Task 30)
|
||||
// Hinweis-Leiste, Kontakt-Leiste, Google und Vorschau (public/zeiten-leisten.js, zeiten-google.js, Vorschau oben in zeiten-editor.js; Spec 3.4 Bereiche 4 bis 6)
|
||||
const ymdKurz = (tag) => `${tag.slice(8)}.${tag.slice(5, 7)}.${tag.slice(0, 4)}`
|
||||
const wochentagNr = (tag) => ((new Date(`${tag}T00:00:00Z`).getUTCDay() + 6) % 7) + 1
|
||||
const naechster = (nr) => { for (let i = 0; i < 7; i += 1) if (wochentagNr(tagPlus(HEUTE, i)) === nr) return tagPlus(HEUTE, i); throw new Error('unmöglich') }
|
||||
const hinweisAktion = { id: 'h_aktion001', art: 'aktion', text: '20 % auf alle Torten', knopf: { art: 'link', ziel: 'https://beispiel.de/torten', text: 'Mehr erfahren' }, von: tagPlus(HEUTE, -1), bis: tagPlus(HEUTE, 10) }
|
||||
const hinweisUrlaub = { id: 'h_urlaub001', art: 'urlaub', text: 'Betriebsurlaub im Sommer', knopf: null, von: tagPlus(HEUTE, 30), bis: tagPlus(HEUTE, 40) }
|
||||
const hinweisAlt = { id: 'h_vorbei001', art: 'info', text: 'Alter Hinweis', knopf: null, von: tagPlus(HEUTE, -20), bis: tagPlus(HEUTE, -10) }
|
||||
|
||||
const hinweisFeld = (fx, form, name) => fx.finde(form, (el) => el.nodeType === 1 && new RegExp(`^hinweise\\.[^.]+\\.${name.replace('.', '\\.')}$`).test(el.getAttribute('data-feld') || ''))
|
||||
const hinweisFehlerPfad = (fx, form, name) => fx.finde(form, (el) => el.nodeType === 1 && new RegExp(`^hinweise\\.[^.]+\\.${name.replace('.', '\\.')}$`).test(el.getAttribute('data-fehler') || ''))
|
||||
const neuerHinweis = (fx, root) => fx.finde(root, 'form[aria-label="Neuer Hinweis"]')
|
||||
const kontaktForm = (fx, root) => fx.finde(root, 'form[aria-label="Kontakt-Leiste"]')
|
||||
const kfeld = (fx, root, name) => fx.finde(kontaktForm(fx, root), `[data-feld="kontakt.${name}"]`)
|
||||
const sichtbar = (el) => el !== null && el.hidden !== true
|
||||
|
||||
/** Zwischenablage des Browsers für die Dauer von fn ersetzen (Node hat seit 21 ein eigenes navigator-Objekt: Eigenschaft sichern und danach zurückgeben). */
|
||||
async function mitZwischenablage(clipboard, fn) {
|
||||
const vorher = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
|
||||
Object.defineProperty(globalThis, 'navigator', { value: clipboard === undefined ? {} : { clipboard }, configurable: true, writable: true, enumerable: true })
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
if (vorher) Object.defineProperty(globalThis, 'navigator', vorher)
|
||||
else delete globalThis.navigator
|
||||
}
|
||||
}
|
||||
|
||||
test('Editor II: die Abschnitte Hinweis-Leiste, Kontakt-Leiste und Google stehen nach den Ausnahmen, erst nach dem ersten Speichern', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
assert.deepEqual(fx.alle(root, '[data-abschnitt]').map((el) => el.getAttribute('data-abschnitt')), ['plaene', 'feiertage', 'ausnahmen', 'hinweise', 'kontakt', 'google'])
|
||||
const leer = await oeffne(fx, { start: stand(null) })
|
||||
for (const key of ['hinweise', 'kontakt', 'google']) assert.equal(abschnitt(fx, leer.root, key), null, `${key}: erst nach dem ersten Speichern`)
|
||||
for (const [bauen, datei] of [['hinweise', 'zeiten-leisten'], ['kontakt', 'zeiten-leisten'], ['google', 'zeiten-google']]) {
|
||||
const modul = await import(`../public/${datei}.js`)
|
||||
assert.equal(typeof modul[bauen], 'function', `${datei}.js exportiert ${bauen}(t)`)
|
||||
}
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Hinweis-Leiste
|
||||
test('Hinweis-Leiste: der Knopf heißt genau „Hinweis-Leiste anlegen“, das Formular hat Text, Art und Zeitraum, PUT mit neuem Hinweis', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const knopf = fx.finde(root, '[data-werkzeug="hinweis-neu"]')
|
||||
assert.equal(fx.text(knopf), 'Hinweis-Leiste anlegen')
|
||||
assert.equal(knopf.tagName, 'BUTTON')
|
||||
assert.match(fx.text(abschnitt(fx, root, 'hinweise')), /Keine Hinweise eingetragen/)
|
||||
await fx.klick(knopf)
|
||||
const form = neuerHinweis(fx, root)
|
||||
assert.ok(form)
|
||||
assert.equal(hinweisFeld(fx, form, 'text').getAttribute('maxlength'), '140')
|
||||
assert.deepEqual(optionen(fx, hinweisFeld(fx, form, 'art')), ['info', 'urlaub', 'aktion', 'wichtig'])
|
||||
assert.deepEqual(fx.alle(hinweisFeld(fx, form, 'art'), 'option').map((o) => fx.text(o)), ['Info', 'Urlaub', 'Aktion', 'Wichtig'])
|
||||
assert.equal(hinweisFeld(fx, form, 'von').getAttribute('type'), 'date')
|
||||
assert.equal(hinweisFeld(fx, form, 'bis').getAttribute('type'), 'date')
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), ' Heute Kuchen im Angebot ')
|
||||
await fx.setze(hinweisFeld(fx, form, 'art'), 'wichtig')
|
||||
const von = tagPlus(HEUTE, 1)
|
||||
const bis = tagPlus(HEUTE, 3)
|
||||
await fx.setze(hinweisFeld(fx, form, 'von'), von)
|
||||
await fx.setze(hinweisFeld(fx, form, 'bis'), bis)
|
||||
const gespeichert = { id: 'h_neu00001', art: 'wichtig', text: 'Heute Kuchen im Angebot', knopf: null, von, bis }
|
||||
fx.antworten.push(antwort(daten({ hinweise: [gespeichert] }), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 1)
|
||||
const h = puts(fx)[0].body.daten.hinweise
|
||||
assert.equal(h.length, 1)
|
||||
assert.match(h[0].id, /^h_[a-z0-9]{8}$/)
|
||||
assert.deepEqual({ ...h[0], id: 'h' }, { id: 'h', art: 'wichtig', text: 'Heute Kuchen im Angebot', knopf: null, von, bis })
|
||||
assert.match(meldung(fx, root), /Hinweis.*gespeichert/)
|
||||
const zeile = fx.finde(abschnitt(fx, root, 'hinweise'), 'li[data-id="h_neu00001"]')
|
||||
assert.ok(zeile, 'der Hinweis steht in der Liste')
|
||||
assert.ok(fx.text(zeile).includes('Heute Kuchen im Angebot'))
|
||||
assert.ok(fx.text(zeile).includes('Wichtig'))
|
||||
assert.ok(fx.text(zeile).includes(ymdKurz(von)))
|
||||
assert.equal(neuerHinweis(fx, root), null, 'das Formular ist nach dem Speichern zu')
|
||||
}))
|
||||
|
||||
test('Hinweis: Zeitraum ist optional (ohne Daten gilt der Hinweis immer), Datum leer ⇒ null', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form = neuerHinweis(fx, root)
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Heute Ruhetag')
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
const h = puts(fx)[0].body.daten.hinweise[0]
|
||||
assert.equal(h.art, 'info')
|
||||
assert.equal(h.von, null)
|
||||
assert.equal(h.bis, null)
|
||||
}))
|
||||
|
||||
test('Hinweis: Aktion ohne Enddatum ⇒ Feldfehler am Enddatum (nichts geht hinaus); % im Text ⇒ Preis-Hinweis', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form = neuerHinweis(fx, root)
|
||||
const hinweis = fx.finde(form, '[data-hinweis="preis"]')
|
||||
assert.ok(hinweis, 'der Preis-Hinweis hat seinen Platz im Formular')
|
||||
assert.equal(sichtbar(hinweis), false, 'zuerst nicht sichtbar')
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Alles 20 % günstiger')
|
||||
assert.equal(sichtbar(hinweis), true, '% im Text')
|
||||
assert.match(fx.text(hinweis), /Endpreise/)
|
||||
assert.match(fx.text(hinweis), /niedrigste/)
|
||||
assert.match(fx.text(hinweis), /30 Tage/)
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Jetzt nur 5 € statt 8 €')
|
||||
assert.equal(sichtbar(hinweis), true, '„statt“ im Text')
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Heute Schlussverkauf')
|
||||
assert.equal(sichtbar(hinweis), false, 'ohne % und „statt“ weg')
|
||||
|
||||
await fx.setze(hinweisFeld(fx, form, 'art'), 'aktion')
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 0, 'ohne Enddatum geht nichts hinaus')
|
||||
assert.equal(fx.text(hinweisFehlerPfad(fx, form, 'bis')), 'Eine Aktion braucht ein Enddatum.')
|
||||
assert.equal(hinweisFeld(fx, form, 'bis').getAttribute('aria-invalid'), 'true')
|
||||
// mit Enddatum geht es hinaus
|
||||
await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 5))
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 1)
|
||||
assert.equal(puts(fx)[0].body.daten.hinweise[0].art, 'aktion')
|
||||
}))
|
||||
|
||||
test('Hinweis: Ende vor Beginn ⇒ Feldfehler; ein Fehler des Servers steht am Feld', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form = neuerHinweis(fx, root)
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Zeitraum falsch')
|
||||
await fx.setze(hinweisFeld(fx, form, 'von'), tagPlus(HEUTE, 5))
|
||||
await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 2))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 0)
|
||||
assert.equal(fx.text(hinweisFehlerPfad(fx, form, 'bis')), 'Das Ende darf nicht vor dem Beginn liegen.')
|
||||
// der Server hat das letzte Wort: Fehler an einem Textfeld
|
||||
await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 6))
|
||||
const pfad = hinweisFeld(fx, form, 'text').getAttribute('data-feld')
|
||||
fx.antworten.push(fehler(400, 'x', { fehler: { [pfad]: 'Bitte gib einen Text ein.' } }))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 1)
|
||||
assert.equal(fx.text(fehlerAn(fx, form, pfad)), 'Bitte gib einen Text ein.')
|
||||
assert.equal(hinweisFeld(fx, form, 'text').getAttribute('aria-invalid'), 'true')
|
||||
}))
|
||||
|
||||
test('Hinweis-Knopf: nur Link (https) oder Anrufen, Link-Prüfung als Hinweis, der Server entscheidet', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form = neuerHinweis(fx, root)
|
||||
const art = hinweisFeld(fx, form, 'knopf.art')
|
||||
assert.deepEqual(optionen(fx, art), ['', 'link', 'anruf'], 'kein Knopf, Link, Anrufen – sonst nichts')
|
||||
assert.deepEqual(fx.alle(art, 'option').map((o) => fx.text(o)), ['Kein Knopf', 'Link', 'Anrufen'])
|
||||
assert.equal(art.value, '', 'Vorgabe: kein Knopf')
|
||||
const felder = fx.finde(form, '[data-knopf-felder]')
|
||||
assert.equal(felder.hidden, true, 'ohne Knopf keine Knopf-Felder')
|
||||
assert.equal(hinweisFeld(fx, form, 'knopf.text').getAttribute('maxlength'), '30')
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Neue Torten')
|
||||
|
||||
await fx.setze(art, 'link')
|
||||
assert.equal(felder.hidden, false)
|
||||
assert.equal(hinweisFeld(fx, form, 'knopf.ziel').getAttribute('maxlength'), '300')
|
||||
const linkHinweis = fx.finde(form, '[data-hinweis="knopf-ziel"]')
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.text'), 'Mehr erfahren')
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'http://beispiel.de/torten')
|
||||
assert.equal(sichtbar(linkHinweis), true, 'http ist kein https')
|
||||
assert.match(fx.text(linkHinweis), /https:\/\//)
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'javascript:alert(1)')
|
||||
assert.equal(sichtbar(linkHinweis), true)
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'https://beispiel.de/torten')
|
||||
assert.equal(sichtbar(linkHinweis), false, 'ein https-Link ist in Ordnung')
|
||||
|
||||
// die Prüfung ist nur ein Hinweis: ein schlechter Link geht hinaus, der Server lehnt ihn ab, der Fehler steht am Feld
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'http://beispiel.de/torten')
|
||||
const pfad = hinweisFeld(fx, form, 'knopf.ziel').getAttribute('data-feld')
|
||||
fx.antworten.push(fehler(400, 'x', { fehler: { [pfad]: 'Bitte gib eine vollständige Adresse mit https:// an (höchstens 300 Zeichen, ohne Zugangsdaten).' } }))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx).length, 1, 'der Server entscheidet')
|
||||
assert.deepEqual(puts(fx)[0].body.daten.hinweise[0].knopf, { art: 'link', ziel: 'http://beispiel.de/torten', text: 'Mehr erfahren' })
|
||||
assert.match(fx.text(fehlerAn(fx, form, pfad)), /https:\/\//)
|
||||
|
||||
// Anrufen: Ziffern, Leerzeichen, + / -
|
||||
await fx.setze(art, 'anruf')
|
||||
const anrufHinweis = fx.finde(form, '[data-hinweis="knopf-ziel"]')
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), '06301 abc')
|
||||
assert.equal(sichtbar(anrufHinweis), true)
|
||||
assert.match(fx.text(anrufHinweis), /Ziffern/)
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), '+49 6301 123-456')
|
||||
assert.equal(sichtbar(anrufHinweis), false)
|
||||
await fx.setze(hinweisFeld(fx, form, 'knopf.text'), 'Anrufen')
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.deepEqual(puts(fx)[1].body.daten.hinweise[0].knopf, { art: 'anruf', ziel: '+49 6301 123-456', text: 'Anrufen' })
|
||||
|
||||
// Knopf wieder „Kein Knopf“ ⇒ knopf null
|
||||
const zwei = await oeffne(fx)
|
||||
await fx.klick(fx.finde(zwei.root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form2 = neuerHinweis(fx, zwei.root)
|
||||
await fx.setze(hinweisFeld(fx, form2, 'text'), 'Ohne Knopf')
|
||||
await fx.setze(hinweisFeld(fx, form2, 'knopf.art'), 'link')
|
||||
await fx.setze(hinweisFeld(fx, form2, 'knopf.art'), '')
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form2, 'button[type="submit"]'))
|
||||
assert.equal(puts(fx)[2].body.daten.hinweise[0].knopf, null)
|
||||
}))
|
||||
|
||||
test('Hinweis bearbeiten und entfernen: der Eintrag wird per ID gefunden, die Reihenfolge der übrigen bleibt, Rückfrage in der Seite', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisAktion, hinweisUrlaub] })) })
|
||||
const liste = abschnitt(fx, root, 'hinweise')
|
||||
const zeile = fx.finde(liste, 'li[data-id="h_aktion001"]')
|
||||
assert.ok(fx.text(zeile).includes('20 % auf alle Torten'))
|
||||
assert.ok(fx.text(zeile).includes('Aktion'))
|
||||
assert.match(fx.text(zeile), /Mehr erfahren/, 'der Knopf steht in der Zeile')
|
||||
await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]'))
|
||||
const form = fx.finde(zeile, 'form[aria-label="Hinweis bearbeiten"]')
|
||||
assert.ok(form)
|
||||
assert.equal(hinweisFeld(fx, form, 'text').value, '20 % auf alle Torten')
|
||||
assert.equal(hinweisFeld(fx, form, 'art').value, 'aktion')
|
||||
assert.equal(hinweisFeld(fx, form, 'knopf.art').value, 'link')
|
||||
assert.equal(hinweisFeld(fx, form, 'knopf.ziel').value, 'https://beispiel.de/torten')
|
||||
assert.equal(hinweisFeld(fx, form, 'knopf.text').value, 'Mehr erfahren')
|
||||
assert.equal(hinweisFeld(fx, form, 'bis').value, hinweisAktion.bis)
|
||||
assert.equal(sichtbar(fx.finde(form, '[data-hinweis="preis"]')), true, '% im gespeicherten Text zeigt den Preis-Hinweis gleich')
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), '30 % auf alle Torten')
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
const h = puts(fx)[0].body.daten.hinweise
|
||||
assert.deepEqual(h.map((x) => x.id), ['h_aktion001', 'h_urlaub001'], 'dieselbe Reihenfolge')
|
||||
assert.equal(h[0].text, '30 % auf alle Torten')
|
||||
assert.deepEqual(h[1], hinweisUrlaub)
|
||||
assert.deepEqual(h[0].knopf, hinweisAktion.knopf)
|
||||
|
||||
// Entfernen mit Rückfrage
|
||||
const zwei = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisAktion, hinweisUrlaub] })) })
|
||||
const zeile2 = fx.finde(zwei.root, 'li[data-id="h_urlaub001"]')
|
||||
await fx.klick(fx.finde(zeile2, '[data-aktion="bearbeiten"]'))
|
||||
await fx.klick(fx.finde(zeile2, '[data-aktion="entfernen"]'))
|
||||
const rueckfrage = fx.finde(zeile2, '.tm-frage-box')
|
||||
assert.match(fx.text(rueckfrage), /Hinweis entfernen\?/)
|
||||
assert.equal(puts(fx).length, 1, 'erst die Rückfrage')
|
||||
fx.antworten.push(antwort(daten({ hinweise: [hinweisAktion] }), 5))
|
||||
await fx.klick(fx.finde(rueckfrage, 'button[type="submit"]'))
|
||||
assert.deepEqual(puts(fx)[1].body.daten.hinweise.map((x) => x.id), ['h_aktion001'])
|
||||
assert.equal(fx.finde(zwei.root, 'li[data-id="h_urlaub001"]'), null, 'der Hinweis ist aus der Liste')
|
||||
}))
|
||||
|
||||
test('Hinweis-Liste: laufend und geplant mit Pille, Vergangenes eingeklappt, nach Beginn sortiert; Text als Text', mitDom({}, async (fx) => {
|
||||
const bose = '<img src=x onerror=alert(1)>'
|
||||
const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisUrlaub, { ...hinweisAktion, text: bose }, hinweisAlt] })) })
|
||||
const liste = abschnitt(fx, root, 'hinweise')
|
||||
assert.equal(fx.alle(root, 'img').length, 0)
|
||||
const kommend = fx.alle(fx.finde(liste, 'ul[aria-label="Laufende und geplante Hinweise"]'), 'li[data-id]').map((li) => li.getAttribute('data-id'))
|
||||
assert.deepEqual(kommend, ['h_aktion001', 'h_urlaub001'], 'nach Beginn sortiert')
|
||||
assert.match(fx.text(fx.finde(liste, 'li[data-id="h_aktion001"]')), /läuft/)
|
||||
assert.match(fx.text(fx.finde(liste, 'li[data-id="h_urlaub001"]')), /geplant/)
|
||||
const vorbei = fx.finde(liste, 'details')
|
||||
assert.match(fx.text(fx.finde(vorbei, 'summary')), /Vergangene Hinweise \(1\)/)
|
||||
assert.ok(fx.finde(vorbei, 'li[data-id="h_vorbei001"]'))
|
||||
}))
|
||||
|
||||
test('Hinweise: bei 20 Hinweisen ist „Hinweis-Leiste anlegen“ gesperrt und sagt warum', mitDom({}, async (fx) => {
|
||||
const viele = Array.from({ length: ZEITEN_LIMITS.hinweise }, (_, i) => ({ id: `h_viele${String(i).padStart(3, '0')}`, art: 'info', text: `Hinweis ${i}`, knopf: null, von: null, bis: null }))
|
||||
const { root } = await oeffne(fx, { start: stand(daten({ hinweise: viele })) })
|
||||
assert.equal(fx.finde(root, '[data-werkzeug="hinweis-neu"]').disabled, true)
|
||||
assert.match(fx.text(abschnitt(fx, root, 'hinweise')), /Mehr als 20 Hinweise sind nicht möglich/)
|
||||
}))
|
||||
|
||||
test('Hinweis speichern zeichnet nur die Hinweise neu: Wochenplan-Entwurf und Kontakt-Eingaben bleiben', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const [von1] = zeitFelder(fx, tagZeile(fx, root, P1, '5'))
|
||||
await fx.setze(von1, '08:15')
|
||||
await fx.setze(kfeld(fx, root, 'adresse'), 'Neue Straße 9')
|
||||
const kontaktAlt = abschnitt(fx, root, 'kontakt')
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]'))
|
||||
const form = neuerHinweis(fx, root)
|
||||
await fx.setze(hinweisFeld(fx, form, 'text'), 'Neu')
|
||||
fx.antworten.push(antwort(daten({ hinweise: [{ id: 'h_neu00002', art: 'info', text: 'Neu', knopf: null, von: null, bis: null }] }), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '08:15', 'der Wochenplan-Entwurf steht noch da')
|
||||
assert.equal(abschnitt(fx, root, 'kontakt'), kontaktAlt, 'der Kontakt-Abschnitt ist dasselbe Element')
|
||||
assert.equal(kfeld(fx, root, 'adresse').value, 'Neue Straße 9')
|
||||
assert.ok(fx.finde(root, 'li[data-id="h_neu00002"]'))
|
||||
assert.equal(puts(fx)[0].body.daten.plaene[0].tage['5'].fenster[0][0], '09:00', 'der Entwurf ging nicht mit hinaus')
|
||||
}))
|
||||
|
||||
test('Urlaub-Frage „Ja, anlegen“: der neue Hinweis steht danach auch in der Hinweis-Liste', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||||
const form = urlaubsformular(fx, root)
|
||||
const von = tagPlus(HEUTE, 20)
|
||||
const bis = tagPlus(HEUTE, 25)
|
||||
await fx.setze(feldIn(fx, form, 'von'), von)
|
||||
await fx.setze(feldIn(fx, form, 'bis'), bis)
|
||||
const urlaub = { id: 'x_urlaub02', art: 'urlaub', von, bis, text: '', modus: 'zu', fenster: [], plan: null }
|
||||
fx.antworten.push(antwort(daten({ ausnahmen: [urlaub] }), 4))
|
||||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||||
const vorschlag = { id: 'h_vorschlag', art: 'urlaub', text: 'Betriebsurlaub', knopf: null, von: tagPlus(von, -7), bis }
|
||||
fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [vorschlag] }), 5))
|
||||
await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-ja"]'))
|
||||
assert.ok(fx.finde(abschnitt(fx, root, 'hinweise'), 'li[data-id="h_vorschlag"]'), 'die Hinweis-Liste zeigt ihn')
|
||||
}))
|
||||
|
||||
test('Hinweise abgeschaltet (nur ansehen): Liste ja, aber kein Knopf zum Anlegen und alle Felder gesperrt', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten({ hinweise: [hinweisAktion] }), { state: 'disabled' }) })
|
||||
const liste = abschnitt(fx, root, 'hinweise')
|
||||
assert.ok(fx.finde(liste, 'li[data-id="h_aktion001"]'))
|
||||
assert.equal(fx.finde(liste, '[data-werkzeug="hinweis-neu"]'), null)
|
||||
await fx.klick(fx.finde(liste, '[data-aktion="bearbeiten"]'))
|
||||
assert.equal(fx.text(fx.finde(liste, '[data-aktion="bearbeiten"]')), 'Ansehen')
|
||||
const form = fx.finde(liste, 'form[aria-label="Hinweis bearbeiten"]')
|
||||
assert.equal(fx.finde(form, 'button[type="submit"]'), null)
|
||||
for (const feld of fx.alle(form, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`)
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Kontakt-Leiste
|
||||
test('Kontakt-Leiste: Ein/Aus, Telefon, Adresse und WhatsApp aus den Daten; unverändert gespeichert bleibt bestaetigtAt (R12)', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const form = kontaktForm(fx, root)
|
||||
assert.ok(form)
|
||||
assert.equal(kfeld(fx, root, 'an').checked, true)
|
||||
assert.equal(kfeld(fx, root, 'telefon').value, '+49 6301123456', 'die gespeicherte Nummer (Ziffern mit Vorwahl) steht mit + da')
|
||||
assert.equal(kfeld(fx, root, 'telefon').getAttribute('maxlength'), '30')
|
||||
assert.equal(kfeld(fx, root, 'adresse').value, KONTAKT.adresse)
|
||||
assert.equal(kfeld(fx, root, 'adresse').getAttribute('maxlength'), '200')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp').checked, true)
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.nummer').value, '+49 17112345678')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.starttext').value, 'Hallo!')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.starttext').getAttribute('maxlength'), '200')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, true, 'schon bestätigt')
|
||||
assert.match(fx.text(form), /Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen/)
|
||||
assert.equal(fx.finde(form, '[data-whatsapp]').hidden, false)
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]'))
|
||||
assert.deepEqual(puts(fx)[0].body.daten.kontakt, KONTAKT, 'dieselben Werte, bestaetigtAt unverändert')
|
||||
}))
|
||||
|
||||
test('Kontakt: das Telefon geht so hinaus, wie der Kunde es schreibt; die Nummer aus der Antwort des Servers steht danach im Feld und in der Meldung', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.setze(kfeld(fx, root, 'telefon'), '0171 1234567')
|
||||
await fx.setze(kfeld(fx, root, 'adresse'), 'Marktplatz 1')
|
||||
const gespeichert = daten({ kontakt: { ...KONTAKT, telefon: '491711234567', adresse: 'Marktplatz 1' } })
|
||||
fx.antworten.push(antwort(gespeichert, 4))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
const k = puts(fx)[0].body.daten.kontakt
|
||||
assert.equal(k.telefon, '0171 1234567')
|
||||
assert.equal(k.adresse, 'Marktplatz 1')
|
||||
assert.equal(kfeld(fx, root, 'telefon').value, '+49 1711234567', 'das Feld zeigt, was der Server gespeichert hat')
|
||||
assert.ok(meldung(fx, root).includes('+49 1711234567'), 'die Meldung nennt die gespeicherte Nummer')
|
||||
assert.match(meldung(fx, root), /Kontakt-Leiste gespeichert/)
|
||||
}))
|
||||
|
||||
test('Kontakt: ohne gespeicherte Kontaktdaten sind die Felder leer und die Leiste aus', mitDom({}, async (fx) => {
|
||||
const ohne = daten()
|
||||
delete ohne.kontakt
|
||||
const { root } = await oeffne(fx, { start: stand(ohne) })
|
||||
assert.equal(kfeld(fx, root, 'an').checked, false)
|
||||
assert.equal(kfeld(fx, root, 'telefon').value, '')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp').checked, false)
|
||||
assert.equal(fx.finde(kontaktForm(fx, root), '[data-whatsapp]').hidden, true)
|
||||
await fx.klick(kfeld(fx, root, 'an'))
|
||||
await fx.setze(kfeld(fx, root, 'telefon'), '06301 123456')
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.deepEqual(puts(fx)[0].body.daten.kontakt, { an: true, telefon: '06301 123456', adresse: '', whatsapp: null })
|
||||
}))
|
||||
|
||||
test('Kontakt: WhatsApp verlangt den Datenschutz-Haken – ohne Haken geht nichts hinaus, mit neuem Haken ein frischer ISO-Zeitpunkt, ein gespeicherter bleibt', mitDom({}, async (fx) => {
|
||||
const ohne = daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: null } })
|
||||
const { root } = await oeffne(fx, { start: stand(ohne) })
|
||||
const form = kontaktForm(fx, root)
|
||||
assert.equal(fx.finde(form, '[data-whatsapp]').hidden, true)
|
||||
await fx.klick(kfeld(fx, root, 'whatsapp'))
|
||||
assert.equal(fx.finde(form, '[data-whatsapp]').hidden, false, 'WhatsApp an zeigt die Felder')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, false)
|
||||
await fx.setze(kfeld(fx, root, 'whatsapp.nummer'), '0171 7654321')
|
||||
await fx.setze(kfeld(fx, root, 'whatsapp.starttext'), 'Hallo, ich habe eine Frage.')
|
||||
await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(puts(fx).length, 0, 'ohne Haken geht nichts hinaus')
|
||||
assert.match(fx.text(fehlerAn(fx, form, 'kontakt.whatsapp.bestaetigtAt')), /Bitte bestätige, dass du den WhatsApp-Hinweis in deine Datenschutzerklärung aufgenommen hast/)
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').getAttribute('aria-invalid'), 'true')
|
||||
// mit Haken: frischer Zeitpunkt (der Server ersetzt ihn durch seine Zeit)
|
||||
await fx.klick(kfeld(fx, root, 'whatsapp.bestaetigtAt'))
|
||||
const vorher = Date.now()
|
||||
const serverZeit = '2026-10-05T08:00:00.000Z'
|
||||
fx.antworten.push(antwort(daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: { nummer: '491717654321', starttext: 'Hallo, ich habe eine Frage.', bestaetigtAt: serverZeit } } }), 4))
|
||||
await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]'))
|
||||
const w = puts(fx)[0].body.daten.kontakt.whatsapp
|
||||
assert.equal(w.nummer, '0171 7654321')
|
||||
assert.equal(w.starttext, 'Hallo, ich habe eine Frage.')
|
||||
assert.match(w.bestaetigtAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
|
||||
assert.ok(Math.abs(Date.parse(w.bestaetigtAt) - vorher) < 5000, 'ein frischer Zeitpunkt')
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.nummer').value, '+49 1717654321', 'die Nummer des Servers')
|
||||
assert.ok(meldung(fx, root).includes('+49 1717654321'))
|
||||
// der Haken steht jetzt (gespeichert): das nächste Speichern schickt den gespeicherten Wert unverändert zurück
|
||||
assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, true)
|
||||
fx.antworten.push(antwort(daten(), 5))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(puts(fx)[1].body.daten.kontakt.whatsapp.bestaetigtAt, serverZeit)
|
||||
}))
|
||||
|
||||
test('Kontakt: WhatsApp abwählen ⇒ whatsapp null (und der Haken ist weg); die Leiste ausschalten ⇒ an false', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(kfeld(fx, root, 'whatsapp'))
|
||||
assert.equal(fx.finde(kontaktForm(fx, root), '[data-whatsapp]').hidden, true)
|
||||
await fx.klick(kfeld(fx, root, 'an'))
|
||||
fx.antworten.push(antwort(daten(), 4))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
const k = puts(fx)[0].body.daten.kontakt
|
||||
assert.equal(k.whatsapp, null)
|
||||
assert.equal(k.an, false)
|
||||
assert.equal(k.telefon, '496301123456', 'die übrigen Angaben bleiben')
|
||||
}))
|
||||
|
||||
test('Kontakt: ein Fehler des Servers steht am Feld; Speichern ändert nichts an den anderen Formularen', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const [von1] = zeitFelder(fx, tagZeile(fx, root, P1, '5'))
|
||||
await fx.setze(von1, '07:45')
|
||||
await fx.setze(kfeld(fx, root, 'telefon'), 'abc')
|
||||
fx.antworten.push(fehler(400, 'x', { fehler: { 'kontakt.telefon': 'Bitte gib eine gültige Telefonnummer an (nur Ziffern, Leerzeichen, + ( ) / . und -).' } }))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.match(fx.text(fehlerAn(fx, root, 'kontakt.telefon')), /gültige Telefonnummer/)
|
||||
assert.equal(kfeld(fx, root, 'telefon').getAttribute('aria-invalid'), 'true')
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '07:45')
|
||||
// Speichern gelingt: nur der Kontakt-Abschnitt wird neu gezeichnet
|
||||
await fx.setze(kfeld(fx, root, 'telefon'), '06301 111')
|
||||
fx.antworten.push(antwort(daten({ kontakt: { ...KONTAKT, telefon: '4963011110' } }), 4))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '07:45', 'der Wochenplan-Entwurf bleibt')
|
||||
}))
|
||||
|
||||
test('Kontakt in der Kundenansicht (R12): der WhatsApp-Haken ist gesperrt, ein gespeicherter Zeitpunkt geht unverändert zurück, nie ein neuer', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(daten(), { kundenansicht: true }), kundenansicht: true })
|
||||
const haken = kfeld(fx, root, 'whatsapp.bestaetigtAt')
|
||||
assert.equal(haken.checked, true)
|
||||
assert.equal(haken.disabled, true, 'gesperrt')
|
||||
assert.match(fx.text(kontaktForm(fx, root)), /Kundenansicht/)
|
||||
assert.match(fx.text(fx.finde(kontaktForm(fx, root), '[data-hinweis="whatsapp-kundenansicht"]')), /nur der Kunde selbst/)
|
||||
await fx.klick(haken)
|
||||
assert.equal(haken.checked, true, 'ein Klick ändert nichts')
|
||||
await fx.setze(kfeld(fx, root, 'whatsapp.starttext'), 'Hallo zusammen')
|
||||
fx.antworten.push(antwort(daten(), 4, { kundenansicht: true }))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(puts(fx)[0].body.daten.kontakt.whatsapp.bestaetigtAt, KONTAKT.whatsapp.bestaetigtAt, 'unverändert zurück')
|
||||
assert.equal(puts(fx)[0].body.daten.kontakt.whatsapp.starttext, 'Hallo zusammen')
|
||||
|
||||
// ohne gespeicherte Bestätigung: WhatsApp einschalten geht in der Kundenansicht nicht (nie ein neuer Zeitpunkt)
|
||||
const ohne = daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: null } })
|
||||
const zwei = await oeffne(fx, { start: stand(ohne, { kundenansicht: true }), kundenansicht: true })
|
||||
await fx.klick(kfeld(fx, zwei.root, 'whatsapp'))
|
||||
await fx.setze(kfeld(fx, zwei.root, 'whatsapp.nummer'), '0171 7654321')
|
||||
const haken2 = kfeld(fx, zwei.root, 'whatsapp.bestaetigtAt')
|
||||
assert.equal(haken2.disabled, true)
|
||||
assert.equal(haken2.checked, false)
|
||||
await fx.klick(fx.finde(kontaktForm(fx, zwei.root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(puts(fx).length, 1, 'es geht nichts hinaus')
|
||||
assert.match(fx.text(fehlerAn(fx, zwei.root, 'kontakt.whatsapp.bestaetigtAt')), /Kunde/)
|
||||
// Kundenansicht ohne WhatsApp: speichern geht, whatsapp bleibt null
|
||||
await fx.klick(kfeld(fx, zwei.root, 'whatsapp'))
|
||||
fx.antworten.push(antwort(daten(), 5, { kundenansicht: true }))
|
||||
await fx.klick(fx.finde(kontaktForm(fx, zwei.root), '[data-aktion="kontakt-speichern"]'))
|
||||
assert.equal(puts(fx)[1].body.daten.kontakt.whatsapp, null)
|
||||
}))
|
||||
|
||||
test('Kontakt abgeschaltet (nur ansehen): Felder gesperrt, kein Speichern-Knopf', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten(), { state: 'disabled' }) })
|
||||
const form = kontaktForm(fx, root)
|
||||
assert.equal(fx.finde(form, 'button[type="submit"]'), null)
|
||||
for (const feld of fx.alle(form, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`)
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Google
|
||||
test('Google: „Für Google kopieren“ legt den googleText des Servers in die Zwischenablage; Link zum Google-Profil, Satz zu den Google-Daten', mitDom({}, async (fx) => {
|
||||
const text = 'Montag: 09:00–12:30, 13:30–18:00\nDienstag: 09:00–18:00\nSonntag: geschlossen'
|
||||
const { root } = await oeffne(fx, { start: stand(daten(), { googleText: text }) })
|
||||
const el = abschnitt(fx, root, 'google')
|
||||
const link = fx.finde(el, 'a[data-google="profil"]')
|
||||
assert.equal(fx.text(link), 'Google-Profil öffnen')
|
||||
assert.equal(link.getAttribute('href'), 'https://business.google.com/')
|
||||
assert.match(link.getAttribute('rel'), /noopener/)
|
||||
assert.equal(link.getAttribute('target'), '_blank')
|
||||
assert.match(fx.text(el), /Google-Daten im Seitencode laufen automatisch/)
|
||||
const knopf = fx.finde(el, '[data-aktion="google-kopieren"]')
|
||||
assert.equal(fx.text(knopf), 'Für Google kopieren')
|
||||
assert.equal(fx.finde(el, '[data-google="text"]').value, text, 'der Text steht zum Nachlesen da')
|
||||
const kopiert = []
|
||||
await mitZwischenablage({ writeText: async (t) => { kopiert.push(t) } }, async () => {
|
||||
await fx.klick(knopf)
|
||||
})
|
||||
assert.deepEqual(kopiert, [text])
|
||||
assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /Kopiert/)
|
||||
// nach einem Speichern gilt der neue Text vom Server
|
||||
const neu = 'Montag: 08:00–12:00\nSonntag: geschlossen'
|
||||
fx.antworten.push(antwort(daten(), 4, { googleText: neu }))
|
||||
await fx.klick(speichernKnopf(fx, root, P1))
|
||||
assert.equal(fx.finde(el, '[data-google="text"]').value, neu, 'die Anzeige folgt dem Server')
|
||||
await mitZwischenablage({ writeText: async (t) => { kopiert.push(t) } }, async () => {
|
||||
await fx.klick(knopf)
|
||||
})
|
||||
assert.deepEqual(kopiert, [text, neu])
|
||||
}))
|
||||
|
||||
test('Google: fehlt die Zwischenablage oder wird das Kopieren abgelehnt, ist der Text markiert und eine Meldung sagt, wie es weitergeht', mitDom({}, async (fx) => {
|
||||
const text = 'Montag: 09:00–18:00'
|
||||
const { root } = await oeffne(fx, { start: stand(daten(), { googleText: text }) })
|
||||
const el = abschnitt(fx, root, 'google')
|
||||
const feld = fx.finde(el, '[data-google="text"]')
|
||||
assert.equal(feld.hasAttribute('readonly'), true)
|
||||
let markiert = 0
|
||||
feld.select = () => { markiert += 1 }
|
||||
await mitZwischenablage(undefined, async () => { await fx.klick(fx.finde(el, '[data-aktion="google-kopieren"]')) })
|
||||
assert.equal(markiert, 1, 'ohne Zwischenablage: Text markiert')
|
||||
assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /markiert/)
|
||||
assert.doesNotMatch(fx.text(fx.finde(el, '[data-meldung="google"]')), /^Kopiert/)
|
||||
await mitZwischenablage({ writeText: async () => { throw new Error('verweigert') } }, async () => { await fx.klick(fx.finde(el, '[data-aktion="google-kopieren"]')) })
|
||||
assert.equal(markiert, 2, 'abgelehnt: Text markiert')
|
||||
assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /markiert/)
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Vorschau
|
||||
const vorschauText = (fx, root) => fx.text(fx.finde(root, '[data-vorschau="jetzt"]'))
|
||||
const regler = (fx, root, name) => fx.finde(root, `[data-regler="${name}"]`)
|
||||
const vorschauAufrufe = (fx) => fx.fetchLog.filter((a) => /\/vorschau\?/.test(a.url))
|
||||
|
||||
test('Vorschau oben: „So steht es gerade auf deiner Seite: …“ aus dem GET, nach dem Speichern aus der Antwort des PUT', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(daten(), { vorschau: { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' } }) })
|
||||
const zeile = fx.finde(root, '.zt-vorschau')
|
||||
assert.ok(zeile)
|
||||
assert.match(fx.text(zeile), /So steht es gerade auf deiner Seite:/)
|
||||
assert.equal(vorschauText(fx, root), 'Jetzt geöffnet · schließt um 18 Uhr')
|
||||
assert.equal(fx.finde(root, '[data-vorschau="jetzt"]').getAttribute('data-offen'), 'true')
|
||||
assert.equal(root.querySelector('.zt-kopfbereich').contains(zeile), true, 'oben, im Kopfbereich')
|
||||
fx.antworten.push(antwort(daten(), 4, { vorschau: { offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' } }))
|
||||
await fx.klick(speichernKnopf(fx, root, P1))
|
||||
assert.equal(vorschauText(fx, root), 'Geschlossen · öffnet morgen um 9 Uhr')
|
||||
assert.equal(fx.finde(root, '[data-vorschau="jetzt"]').getAttribute('data-offen'), 'false')
|
||||
assert.equal(vorschauAufrufe(fx).length, 0, 'ohne Regler keine weitere Anfrage')
|
||||
}))
|
||||
|
||||
test('Vorschau vor dem ersten Speichern: es steht noch nichts da; nach dem ersten Speichern der Status aus der Antwort', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(null) })
|
||||
assert.match(fx.text(fx.finde(root, '.zt-vorschau')), /So steht es gerade auf deiner Seite:/)
|
||||
assert.match(vorschauText(fx, root), /noch keine Öffnungszeiten gespeichert/i)
|
||||
const id = fx.finde(root, 'details[data-plan]').getAttribute('data-plan')
|
||||
fx.antworten.push(antwort(daten({ plaene: [plan(id)] }), 1, { vorschau: { offen: null, text: 'Öffnungszeiten nach Vereinbarung', kurz: 'Nach Vereinbarung' } }))
|
||||
await fx.klick(speichernKnopf(fx, root, id))
|
||||
assert.equal(vorschauText(fx, root), 'Öffnungszeiten nach Vereinbarung')
|
||||
assert.equal(fx.alle(root, '.zt-vorschau').length, 1)
|
||||
}))
|
||||
|
||||
test('Vorschau-Regler: fragt /vorschau mit dem nächsten Vorkommen des Wochentags und der Uhrzeit (Berliner Wanduhr) und zeigt die Antwort', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(daten(), { vorschau: { offen: true, text: 'Jetzt geöffnet', kurz: 'Geöffnet' } }) })
|
||||
const tag = regler(fx, root, 'tag')
|
||||
const zeit = regler(fx, root, 'zeit')
|
||||
assert.deepEqual(optionen(fx, tag), ['1', '2', '3', '4', '5', '6', '7'])
|
||||
assert.deepEqual(fx.alle(tag, 'option').map((o) => fx.text(o)), ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'])
|
||||
assert.equal(tag.value, String(wochentagNr(HEUTE)), 'Vorgabe: heute')
|
||||
assert.equal(zeit.getAttribute('type'), 'time')
|
||||
assert.match(zeit.value, /^\d{2}:\d{2}$/, 'Vorgabe: jetzt (Berliner Zeit)')
|
||||
assert.match(fx.text(root.querySelector('.zt-regler')), /Uhrzeit testen/)
|
||||
|
||||
const geschlossen = { status: 200, body: { offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' } }
|
||||
fx.antworten.push(geschlossen, geschlossen) // eine Anfrage je Änderung
|
||||
await fx.setze(tag, '3')
|
||||
await fx.setze(zeit, '14:30')
|
||||
const aufrufe = vorschauAufrufe(fx)
|
||||
assert.ok(aufrufe.length >= 1)
|
||||
const letzter = aufrufe[aufrufe.length - 1]
|
||||
assert.equal(letzter.method, 'GET')
|
||||
const url = new URL(letzter.url, 'http://x')
|
||||
assert.equal(url.pathname, '/api/zeiten/p1/vorschau')
|
||||
assert.equal(url.searchParams.get('zeit'), `${naechster(3)}T14:30`)
|
||||
const test = fx.text(fx.finde(root, '[data-vorschau="test"]'))
|
||||
assert.match(test, /Geschlossen · öffnet morgen um 9 Uhr/)
|
||||
assert.ok(test.includes('Mittwoch'), 'der Tag der Anfrage steht dabei')
|
||||
assert.ok(test.includes('14:30'))
|
||||
assert.equal(vorschauText(fx, root), 'Jetzt geöffnet', 'die Vorschau „jetzt“ bleibt')
|
||||
}))
|
||||
|
||||
test('Vorschau-Regler: 409 keine_zeiten ist ein Hinweis, kein Fehler; ein anderer Fehler steht als Fehler; leere Uhrzeit fragt nichts', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { start: stand(null) })
|
||||
fx.antworten.push(fehler(409, 'Bitte speichere zuerst deine Öffnungszeiten.', { code: 'keine_zeiten' }))
|
||||
await fx.setze(regler(fx, root, 'zeit'), '10:00')
|
||||
const ergebnis = fx.finde(root, '[data-vorschau="test"]')
|
||||
assert.match(fx.text(ergebnis), /Speichere zuerst deine Öffnungszeiten/)
|
||||
assert.notEqual(ergebnis.getAttribute('role'), 'alert', 'kein Fehler')
|
||||
assert.equal(root.querySelector('[data-meldung="editor"]').textContent, '', 'nichts über dem Editor')
|
||||
// anderer Fehler
|
||||
fx.antworten.push(fehler(500, 'Fehler 500'))
|
||||
await fx.setze(regler(fx, root, 'zeit'), '11:00')
|
||||
assert.equal(fx.finde(root, '[data-vorschau="test"]').getAttribute('role'), 'alert')
|
||||
assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Fehler 500/)
|
||||
// Uhrzeit leer ⇒ keine Anfrage
|
||||
const n = vorschauAufrufe(fx).length
|
||||
await fx.setze(regler(fx, root, 'zeit'), '')
|
||||
assert.equal(vorschauAufrufe(fx).length, n)
|
||||
}))
|
||||
|
||||
test('Vorschau-Regler: nur die letzte Anfrage zählt – eine späte Antwort auf eine frühere überschreibt nichts', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
const echtes = globalThis.fetch
|
||||
let freigeben
|
||||
const wartet = new Promise((resolve) => { freigeben = resolve })
|
||||
let nr = 0
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (!/\/vorschau\?/.test(String(url))) return echtes(url, optionen)
|
||||
nr += 1
|
||||
const meine = nr
|
||||
if (meine === 1) await wartet // die erste Antwort kommt zuletzt
|
||||
fx.fetchLog.push({ method: 'GET', url: String(url) })
|
||||
return { ok: true, status: 200, json: async () => ({ offen: meine === 1, text: meine === 1 ? 'Erste Frage' : 'Zweite Frage', kurz: '' }) }
|
||||
}
|
||||
try {
|
||||
const erste = fx.setze(regler(fx, root, 'zeit'), '09:00')
|
||||
await fx.ruhe()
|
||||
await fx.setze(regler(fx, root, 'zeit'), '20:00')
|
||||
assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Zweite Frage/)
|
||||
freigeben()
|
||||
await erste
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Zweite Frage/, 'die späte erste Antwort wird verworfen')
|
||||
assert.doesNotMatch(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Erste Frage/)
|
||||
} finally {
|
||||
globalThis.fetch = echtes
|
||||
}
|
||||
}))
|
||||
|
||||
test('Vorschau-Regler: nach dem Speichern wird dieselbe Uhrzeit mit den neuen Daten erneut gefragt', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
fx.antworten.push({ status: 200, body: { offen: true, text: 'Alt', kurz: '' } })
|
||||
await fx.setze(regler(fx, root, 'zeit'), '10:00')
|
||||
assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Alt/)
|
||||
fx.antworten.push(antwort(daten(), 4), { status: 200, body: { offen: false, text: 'Neu', kurz: '' } })
|
||||
await fx.klick(speichernKnopf(fx, root, P1))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Neu/)
|
||||
assert.equal(new URL(vorschauAufrufe(fx).at(-1).url, 'http://x').searchParams.get('zeit').slice(-5), '10:00')
|
||||
}))
|
||||
|
||||
test('Vorschau abgeschaltet (nur ansehen): keine Vorschau und kein Regler (die Website zeigt nichts mehr)', mitDom({}, async (fx) => {
|
||||
const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten(), { state: 'disabled' }) })
|
||||
assert.equal(fx.finde(root, '.zt-vorschau'), null)
|
||||
assert.equal(fx.finde(root, '[data-regler]'), null)
|
||||
}))
|
||||
|
||||
test('naechstesVorkommen: der erste Tag ab heute (einschließlich) mit dem Wochentag', async () => {
|
||||
const { naechstesVorkommen, wochentagVon } = await import('../public/zeiten-view.js')
|
||||
assert.equal(wochentagVon('2026-10-05'), 1, 'Montag')
|
||||
assert.equal(wochentagVon('2026-10-11'), 7, 'Sonntag')
|
||||
assert.equal(naechstesVorkommen('2026-10-05', 1), '2026-10-05', 'heute zählt')
|
||||
assert.equal(naechstesVorkommen('2026-10-05', 3), '2026-10-07')
|
||||
assert.equal(naechstesVorkommen('2026-10-07', 1), '2026-10-12', 'nächste Woche')
|
||||
assert.equal(naechstesVorkommen('2026-10-05', 7), '2026-10-11')
|
||||
assert.equal(naechstesVorkommen('2026-12-30', 5), '2027-01-01', 'über den Jahreswechsel')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- Quelltext
|
||||
const ohneKommentare = (quelle) => quelle.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
|
||||
const DATEIEN = ['zeiten-speichern.js', 'zeiten-editor.js', 'zeiten-plaene.js', 'zeiten-ausnahmen.js']
|
||||
const DATEIEN = ['zeiten-speichern.js', 'zeiten-editor.js', 'zeiten-plaene.js', 'zeiten-ausnahmen.js', 'zeiten-leisten.js', 'zeiten-google.js']
|
||||
|
||||
test('Quelltext: vier neue Dateien, nur relative Importe, kein HTML aus Daten, keine Browser-Rückfragen, kein Speicher, Du-Form', () => {
|
||||
test('Quelltext: sechs Editor-Dateien, nur relative Importe, kein HTML aus Daten, keine Browser-Rückfragen, kein Speicher, Du-Form', () => {
|
||||
const vorhanden = readdirSync(new URL('../public/', import.meta.url))
|
||||
for (const datei of DATEIEN) {
|
||||
assert.ok(vorhanden.includes(datei), `${datei} fehlt`)
|
||||
|
||||
Reference in new issue
Block a user