- JSON-LD wird ohne "<" geschrieben (<), ein </script> im Firmennamen bricht keinen Block mehr - verschiebe(): ist die Hinweis-Leiste weggescrollt, bekommen feste Elemente ihren eigenen Inline-top zurück (Köpfe, die per Klasse beim Scrollen wegfahren, funktionieren wieder) - Späte Platzhalter (Einzelseiten-Apps): entprellter MutationObserver auf body zeichnet neue oder wieder eingehängte Platzhalter nach - sichererLink lehnt Zugangsdaten in der Adresse ab; die Adresse der Schnittstelle bestimmt nur das eigene Script-Tag, kein Platzhalter mehr (der siteKey am Platzhalter bleibt laut Spec 4.2) - Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
602 lines
39 KiB
JavaScript
602 lines
39 KiB
JavaScript
// Playwright-Checks des Website-Skripts public/embed/zeiten.js (Oeffnungszeiten live, Aufgabe 26: Hinweis-Leiste, Kontakt-Leiste,
|
||
// JSON-LD, Platzhalter) gegen den UI-Testserver (test/ui/zeitenTestServer.mjs): echte oeffentliche Schnittstelle, Nachbau von Appwrite.
|
||
// Nicht Teil von npm test.
|
||
// Aufruf: free -m && nice node test/ui/zeiten-embed.ui.mjs (es laeuft nur ein Browser)
|
||
// Screenshots: /root/reports/oeffnungszeiten-ui/embed-*.png (bei Fehlern fehler-embed-<Pruefung>.png). Exit-Code 1, wenn eine Pruefung scheitert.
|
||
//
|
||
// Seiten: Sie laufen auf srv.pageOrigin (http://localhost:PORT) und sprechen die Schnittstelle unter srv.url (http://127.0.0.1:PORT)
|
||
// an: echte Cross-Origin-Anfragen wie auf Kundenseiten. Server-Uhr: Montag 2026-10-05 10:00 Berliner Zeit (fest, srv.setzeUhr);
|
||
// Pruefungen mit Zeitsprung installieren dieselbe Zeit im Browser (context.clock.install) und spulen mit fastForward vor.
|
||
import { mkdir, readdir, rm } from 'node:fs/promises'
|
||
import path from 'node:path'
|
||
import { startZeitenUiServer, zeitenEingabe, hinweis, FIRMA, ADRESSE } from './zeitenTestServer.mjs'
|
||
|
||
const PW = process.env.PLAYWRIGHT_CORE || '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
|
||
const { chromium } = await import(PW)
|
||
|
||
const SHOTS = '/root/reports/oeffnungszeiten-ui'
|
||
await mkdir(SHOTS, { recursive: true })
|
||
for (const datei of await readdir(SHOTS)) if (/^(embed|fehler-embed)-.*\.png$/.test(datei)) await rm(path.join(SHOTS, datei)) // alte Laeufe
|
||
|
||
const srv = await startZeitenUiServer()
|
||
const KEY = srv.SITE_KEY
|
||
const API = `${srv.url}/api/public/zeiten/v1/${KEY}`
|
||
|
||
// ---------------------------------------------------------------- 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 nahe = (a, b, tol = 1) => Math.abs(a - b) <= tol
|
||
const fmt = (z) => (typeof z === 'number' ? z.toFixed(1) : String(z))
|
||
|
||
const SKRIPT = `<script src="/embed/zeiten.js" data-site="${KEY}" data-api="${srv.url}" defer></script>`
|
||
const STATUS = '<p>Status: <span id="status" data-webklar-zeiten data-modus="status">Ersatz-Status</span></p>'
|
||
const lang = (n = 40) => Array.from({ length: n }, (_, i) => `<p>Absatz ${i + 1}: Brot, Brötchen und Kuchen aus eigener Backstube.</p>`).join('\n')
|
||
|
||
/** Testseite: kopf = <head>-Zusatz, stil = CSS, anfang = Anfang von <body> (z. B. Kopfzeile), inhalt = <main>. */
|
||
function seitenHtml({ kopf = '', stil = '', anfang = '', inhalt = `${STATUS}${lang()}<p id="ende">Ende der Seite</p>`, skript = SKRIPT } = {}) {
|
||
return `<!doctype html>
|
||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Bäckerei Sonnenschein</title>
|
||
<style>body{margin:0;font-family:system-ui,sans-serif;background:#faf7f2;color:#1f2937}main{max-width:760px;margin:0 auto;padding:16px}${stil}</style>${kopf}</head>
|
||
<body>${anfang}<main>${inhalt}</main>${skript}</body></html>`
|
||
}
|
||
|
||
const KOPF_FEST = '<header id="kopf" style="position:fixed;top:0;left:0;right:0;height:60px;background:#222;color:#fff;z-index:100;padding:0 16px;line-height:60px">Bäckerei Sonnenschein</header>'
|
||
const KOPF_KLEBT = '<header id="kopf" style="position:sticky;top:0;height:60px;background:#222;color:#fff;z-index:100;padding:0 16px;line-height:60px">Bäckerei Sonnenschein</header>'
|
||
|
||
const HOST = (art) => `[data-wkz-leiste="${art}"]`
|
||
const anzahl = (page, sel) => page.locator(sel).count()
|
||
const hoehe = (page, sel) => page.evaluate((s) => document.querySelector(s).getBoundingClientRect().height, sel)
|
||
const oben = (page, sel) => page.evaluate((s) => document.querySelector(s).getBoundingClientRect().top, sel)
|
||
const scrolle = (page, y) => page.evaluate((v) => window.scrollTo(0, v), y)
|
||
const shot = async (page, name) => {
|
||
await page.waitForTimeout(450) // die Einblendung der Leiste (0,3 s) ist fertig
|
||
await page.screenshot({ path: `${SHOTS}/embed-${name}.png` })
|
||
}
|
||
|
||
/** Wartet, bis das Skript die Platzhalter gefuellt hat (und damit in derselben Runde die Leisten gezeichnet). */
|
||
const warteBereit = (page) => page.waitForFunction(() => { const s = document.getElementById('status'); return s && !s.textContent.includes('Ersatz') })
|
||
|
||
/** Wartet auf die Antwort der Schnittstelle und ein paar Bilder danach (fuer Seiten, auf denen nichts gefuellt wird). */
|
||
async function warteAufSchnittstelle(page, aktion) {
|
||
const antwort = page.waitForResponse((r) => r.url().includes('/api/public/zeiten/v1/'))
|
||
await aktion()
|
||
await antwort
|
||
await page.waitForTimeout(400)
|
||
}
|
||
|
||
const STATUS_OFFEN = 'Jetzt geöffnet · schließt um 18 Uhr'
|
||
const STATUS_ZU = 'Geschlossen · öffnet morgen um 9 Uhr'
|
||
const statusText = (page) => page.locator('#status').innerText()
|
||
|
||
const results = []
|
||
let browser = null
|
||
const hosts = new Set()
|
||
|
||
async function check(name, fn, { eingabe, team, viewport = { width: 1280, height: 800 }, uhr, context = {}, initScript } = {}) {
|
||
await srv.zuruecksetzen({ eingabe, team })
|
||
if (uhr) srv.setzeUhr(uhr)
|
||
const ctx = await browser.newContext({ viewport, ...context })
|
||
if (uhr) await ctx.clock.install({ time: new Date(uhr) })
|
||
const page = await ctx.newPage()
|
||
page.setDefaultTimeout(8000)
|
||
const fehler = []
|
||
page.on('pageerror', (e) => fehler.push(e.message))
|
||
page.on('request', (r) => { try { const u = new URL(r.url()); if (u.protocol.startsWith('http')) hosts.add(u.host) } catch { /* data: u. a. */ } })
|
||
if (initScript) await page.addInitScript(initScript)
|
||
try {
|
||
await fn(page, ctx)
|
||
assert(fehler.length === 0, `JavaScript-Fehler auf der Seite: ${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]}`)
|
||
await page.screenshot({ path: `${SHOTS}/fehler-embed-${name}.png`, fullPage: true }).catch(() => {})
|
||
} finally {
|
||
await ctx.close()
|
||
}
|
||
}
|
||
|
||
const INFO = hinweis('info', 'Heute frische Brezeln aus dem Ofen.')
|
||
const HANDY = { width: 390, height: 844 }
|
||
|
||
// ---------------------------------------------------------------- Pruefungen
|
||
|
||
async function pruefungen() {
|
||
await check('hinweisleiste_zeigt_gueltigen_hinweis', async (page) => {
|
||
await page.goto(srv.seite('hinweis.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const host = page.locator(HOST('hinweis'))
|
||
await host.waitFor({ state: 'attached' })
|
||
const region = page.getByRole('region', { name: 'Hinweis' })
|
||
assert(await region.isVisible(), 'Region "Hinweis" nicht sichtbar')
|
||
assert((await region.innerText()).includes('Heute frische Brezeln aus dem Ofen.'), 'Text fehlt')
|
||
eq(await region.locator('button, [role=button]').count(), 0, 'Schliessen-Knopf vorhanden')
|
||
assert(!(await region.innerText()).includes('Laden schließt'), 'der Hinweis von morgen gilt noch nicht')
|
||
const farben = await region.evaluate((e) => { const c = getComputedStyle(e); return [c.backgroundColor, c.color] })
|
||
eq(farben, ['rgb(224, 242, 254)', 'rgb(12, 74, 110)'], 'Farben der Art Info')
|
||
// erstes Kind von body, im Fluss (schiebt den Inhalt)
|
||
assert(await page.evaluate(() => document.body.firstElementChild.hasAttribute('data-wkz-leiste')), 'Host ist nicht das erste Kind von body')
|
||
assert(await page.evaluate(() => ['static', 'relative'].includes(getComputedStyle(document.querySelector('[data-wkz-leiste="hinweis"]')).position)), 'Host muss im Fluss stehen')
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(h > 20, `Hoehe der Leiste ${fmt(h)}`)
|
||
assert(nahe(await oben(page, 'main'), h, 1.5), `main steht bei ${fmt(await oben(page, 'main'))} statt unter der Leiste (${fmt(h)})`)
|
||
await shot(page, 'hinweis-desktop')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO, hinweis('wichtig', 'Nur heute: Laden schließt um 14 Uhr.', { von: '2026-10-06', bis: '2026-10-06' })] }) })
|
||
|
||
await check('hinweis_wichtig_mit_knopf', async (page) => {
|
||
await page.goto(srv.seite('hinweis-wichtig.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const region = page.getByRole('region', { name: 'Hinweis' })
|
||
await region.waitFor()
|
||
assert((await region.innerText()).includes('Heute Betriebsfeier'), 'der Hinweis mit dem hoechsten Rang gewinnt')
|
||
const farben = await region.evaluate((e) => { const c = getComputedStyle(e); return [c.backgroundColor, c.color] })
|
||
eq(farben, ['rgb(185, 28, 28)', 'rgb(255, 255, 255)'], 'Farben der Art Wichtig')
|
||
const knopf = region.getByRole('link', { name: 'Zur Aktion' })
|
||
eq(await knopf.getAttribute('href'), 'https://baeckerei.example/aktion?x=1', 'href')
|
||
eq(await knopf.getAttribute('rel'), 'noopener noreferrer', 'rel')
|
||
const box = await knopf.boundingBox()
|
||
assert(box.height >= 44 && box.width >= 44, `Tippflaeche ${box.width}x${box.height}`)
|
||
// sichtbarer Fokus
|
||
await knopf.focus()
|
||
const fokus = await knopf.evaluate((e) => getComputedStyle(e).outlineStyle)
|
||
assert(fokus !== 'none', 'kein sichtbarer Fokus')
|
||
await shot(page, 'hinweis-wichtig')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO, hinweis('wichtig', 'Heute Betriebsfeier: wir schließen um 14 Uhr.', { knopf: { art: 'link', ziel: 'https://baeckerei.example/aktion?x=1', text: 'Zur Aktion' } })] }) })
|
||
|
||
await check('hinweisleiste_aus', async (page) => {
|
||
await page.goto(srv.seite('hinweis-aus.html', seitenHtml({ anfang: KOPF_FEST })))
|
||
await warteBereit(page) // das Skript hat gelaufen und die Platzhalter gefuellt ...
|
||
eq(await anzahl(page, HOST('hinweis')), 0, 'Leiste trotz Team-Schalter')
|
||
// ... und nichts an der Seite veraendert
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.top), '0px', 'Kopf angefasst')
|
||
eq(await page.evaluate(() => document.documentElement.style.scrollPaddingTop), '', 'scroll-padding-top angefasst')
|
||
assert(nahe(await oben(page, '#kopf'), 0, 0.5), 'Kopf nicht bei 0')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), team: { hinweisleisteAus: true } })
|
||
|
||
await check('fester_header_rueckt', async (page) => {
|
||
await page.goto(srv.seite('fest.html', seitenHtml({ anfang: KOPF_FEST, inhalt: `<div style="height:60px"></div>${STATUS}${lang()}` })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '#kopf'), h, 1), `Kopf steht bei ${fmt(await oben(page, '#kopf'))}, die Leiste ist ${fmt(h)} hoch`)
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.getPropertyPriority('top')), 'important', 'Prioritaet des Inline-top')
|
||
// scroll-padding-top um die Hoehe erhoeht
|
||
const sp = await page.evaluate(() => parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0)
|
||
assert(nahe(sp, h, 1), `scroll-padding-top ${fmt(sp)} statt ${fmt(h)}`)
|
||
// halb gescrollt: der Kopf folgt dem unteren Rand der Leiste
|
||
const halb = Math.round(h / 2)
|
||
await scrolle(page, halb)
|
||
await page.waitForFunction(([erwartet]) => Math.abs(document.getElementById('kopf').getBoundingClientRect().top - erwartet) < 1, [h - halb])
|
||
// weiter gescrollt: wieder bei 0
|
||
await scrolle(page, 400)
|
||
await page.waitForFunction(() => Math.abs(document.getElementById('kopf').getBoundingClientRect().top) < 0.5)
|
||
// zurueck nach oben: wieder unter der Leiste
|
||
await scrolle(page, 0)
|
||
await page.waitForFunction(([erwartet]) => Math.abs(document.getElementById('kopf').getBoundingClientRect().top - erwartet) < 1, [h])
|
||
await shot(page, 'fester-header')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('fester_header_mit_polster_bleibt', async (page) => {
|
||
// Die Seite haelt Platz fuer ihren festen Kopf per body-Polster frei: die Leiste steht erst darunter, der Kopf muss bleiben, wo er ist
|
||
await page.goto(srv.seite('polster.html', seitenHtml({ anfang: KOPF_FEST, stil: 'body{padding-top:60px}' })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
assert(nahe(await oben(page, HOST('hinweis')), 60, 1), `Leiste steht bei ${fmt(await oben(page, HOST('hinweis')))} statt unter dem Polster`)
|
||
assert(nahe(await oben(page, '#kopf'), 0, 0.5), `der Kopf wurde gerueckt (${fmt(await oben(page, '#kopf'))})`)
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.getPropertyPriority('top')), '', 'Inline-top des Kopfes angefasst')
|
||
await shot(page, 'fester-header-polster')
|
||
await scrolle(page, 200)
|
||
await page.waitForTimeout(100)
|
||
assert(nahe(await oben(page, '#kopf'), 0, 0.5), 'Kopf nach dem Scrollen nicht bei 0')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('klebender_header', async (page) => {
|
||
await page.goto(srv.seite('klebt.html', seitenHtml({ anfang: KOPF_KLEBT })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '#kopf'), h, 1), `klebender Kopf steht bei ${fmt(await oben(page, '#kopf'))}, die Leiste ist ${fmt(h)} hoch`)
|
||
const halb = Math.round(h / 2)
|
||
await scrolle(page, halb)
|
||
await page.waitForFunction(([erwartet]) => Math.abs(document.getElementById('kopf').getBoundingClientRect().top - erwartet) < 1, [h - halb])
|
||
await scrolle(page, 500)
|
||
await page.waitForFunction(() => Math.abs(document.getElementById('kopf').getBoundingClientRect().top) < 0.5)
|
||
await scrolle(page, 0)
|
||
await page.waitForFunction(([erwartet]) => Math.abs(document.getElementById('kopf').getBoundingClientRect().top - erwartet) < 1, [h])
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('fester_header_versteckt_beim_scrollen', async (page) => {
|
||
// Kopf, der per Klasse beim Scrollen wegfaehrt (top: -100px), mit dem eigenen Scroll-Skript der Seite. Solange die Hinweis-Leiste sichtbar ist,
|
||
// rueckt das Skript ihn; ist sie weggescrollt, muss die Klasse der Seite wieder gelten (kein dauerhaftes top: 0 !important).
|
||
const kopf = '<header id="kopf" style="position:fixed;left:0;right:0;height:60px;background:#222;color:#fff;z-index:100;padding:0 16px;line-height:60px">Bäckerei Sonnenschein</header>'
|
||
+ '<script>addEventListener("scroll",function(){document.getElementById("kopf").classList.toggle("weg",scrollY>300)},{passive:true})</script>'
|
||
await page.goto(srv.seite('kopf-weg.html', seitenHtml({ anfang: kopf, stil: '#kopf{top:0}#kopf.weg{top:-100px}' })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '#kopf'), h, 1), `Kopf steht bei ${fmt(await oben(page, '#kopf'))} statt unter der Leiste (${fmt(h)})`)
|
||
// weit gescrollt: die Seite blendet den Kopf aus, das Skript haelt ihn nicht fest
|
||
await scrolle(page, 700)
|
||
await page.waitForFunction(() => document.getElementById('kopf').getBoundingClientRect().top < -99)
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.getPropertyValue('top')), '', 'Inline-top des Kopfes nach dem Wegscrollen der Leiste')
|
||
// zurueck nach oben: die Klasse faellt ab, der Kopf steht wieder unter der Leiste
|
||
await scrolle(page, 0)
|
||
await page.waitForFunction(([erwartet]) => Math.abs(document.getElementById('kopf').getBoundingClientRect().top - erwartet) < 1, [h])
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('team_navselektor', async (page) => {
|
||
// der Schwebende hat top: 10px (nicht top: 0): nur der Selektor des Teams findet ihn
|
||
const seite = seitenHtml({
|
||
stil: '.schweber{position:fixed;top:10px;left:16px;z-index:100;background:#222;color:#fff;padding:8px 12px;border-radius:8px}',
|
||
anfang: '<nav class="schweber">Menü</nav>',
|
||
})
|
||
await page.goto(srv.seite('nav.html', seite))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '.schweber'), 10 + h, 1), `Schweber steht bei ${fmt(await oben(page, '.schweber'))} statt ${fmt(10 + h)}`)
|
||
await scrolle(page, 400)
|
||
await page.waitForFunction(() => Math.abs(document.querySelector('.schweber').getBoundingClientRect().top - 10) < 0.5)
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), team: { navSelektor: '.schweber' } })
|
||
|
||
await check('ungueltiger_selektor', async (page) => {
|
||
await page.goto(srv.seite('selektor.html', seitenHtml({ anfang: KOPF_FEST })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' }) // die Leiste ist trotzdem da
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '#kopf'), h, 1), 'der feste Kopf wird auch ohne gueltigen Selektor erkannt und gerueckt')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), team: { navSelektor: '[[kaputt' } })
|
||
|
||
await check('hinweis_endet_und_stellt_wieder_her', async (page, ctx) => {
|
||
await page.goto(srv.seite('ende.html', seitenHtml({ anfang: KOPF_FEST })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
const h = await hoehe(page, HOST('hinweis'))
|
||
assert(nahe(await oben(page, '#kopf'), h, 1), 'Kopf unter der Leiste')
|
||
// 23:55 Berlin: der Hinweis gilt bis 24:00 und verschwindet dann von selbst
|
||
await ctx.clock.fastForward(6 * 60 * 1000)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'detached' })
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.top), '0px', 'Inline-top des Kopfes nicht zurueckgesetzt')
|
||
eq(await page.evaluate(() => document.getElementById('kopf').style.getPropertyPriority('top')), '', 'Prioritaet nicht zurueckgesetzt')
|
||
eq(await page.evaluate(() => document.documentElement.style.scrollPaddingTop), '', 'scroll-padding-top nicht zurueckgesetzt')
|
||
assert(nahe(await oben(page, '#kopf'), 0, 0.5), 'Kopf wieder bei 0')
|
||
assert(nahe(await oben(page, 'main'), 0, 0.5), 'Inhalt rueckt wieder nach oben')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [hinweis('info', 'Nur heute: Kuchen zum halben Preis.', { von: '2026-10-05', bis: '2026-10-05' })] }), uhr: '2026-10-05T21:55:00.000Z' })
|
||
|
||
await check('kontaktleiste_nur_handy', async (page) => {
|
||
await page.goto(srv.seite('handy.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const host = page.locator(HOST('kontakt'))
|
||
await host.waitFor({ state: 'attached' })
|
||
const stil = await host.evaluate((e) => { const c = getComputedStyle(e); return [c.position, c.zIndex, c.bottom] })
|
||
eq(stil, ['fixed', '40', '0px'], 'position, z-index, bottom der Kontakt-Leiste')
|
||
const knoepfe = host.locator('a')
|
||
eq(await knoepfe.count(), 3, 'Anzahl Knoepfe')
|
||
eq(await knoepfe.allInnerTexts(), ['Anrufen', 'Route', 'WhatsApp'], 'Beschriftungen')
|
||
const hrefs = await knoepfe.evaluateAll((l) => l.map((a) => a.getAttribute('href')))
|
||
eq(hrefs[0], 'tel:+491711234567', 'tel:')
|
||
assert(hrefs[1].startsWith('https://www.google.com/maps/dir/'), `Route: ${hrefs[1]}`)
|
||
assert(hrefs[2].startsWith('https://wa.me/491711234567'), `WhatsApp: ${hrefs[2]}`)
|
||
for (const a of await knoepfe.elementHandles()) {
|
||
const b = await a.boundingBox()
|
||
assert(b.height >= 44 && b.width >= 44, `Tippflaeche ${b.width}x${b.height}`)
|
||
eq(await a.getAttribute('rel'), 'noopener noreferrer', 'rel')
|
||
}
|
||
// Abstandhalter am Ende von body in Hoehe der Leiste, scroll-padding-bottom
|
||
const h = await hoehe(page, HOST('kontakt'))
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 1, 'Abstandhalter')
|
||
assert(await page.evaluate(() => document.body.lastElementChild.hasAttribute('data-wkz-abstand')), 'Abstandhalter nicht am Ende von body')
|
||
assert(nahe(await hoehe(page, '[data-wkz-abstand]'), h, 1), 'Abstandhalter nicht so hoch wie die Leiste')
|
||
assert(nahe(await page.evaluate(() => parseFloat(getComputedStyle(document.documentElement).scrollPaddingBottom) || 0), h, 1), 'scroll-padding-bottom')
|
||
// ganz unten: der letzte Absatz liegt ueber der Leiste
|
||
await scrolle(page, 1e6)
|
||
await page.waitForFunction(() => window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 1)
|
||
const unten = await page.evaluate(() => document.getElementById('ende').getBoundingClientRect().bottom)
|
||
const leisteOben = await oben(page, HOST('kontakt'))
|
||
assert(unten <= leisteOben + 0.5, `der letzte Absatz endet bei ${fmt(unten)}, die Leiste beginnt bei ${fmt(leisteOben)}`)
|
||
await shot(page, 'kontakt-handy')
|
||
// auf breitem Schirm weg (und wieder da: die Abfrage wird beobachtet)
|
||
await page.setViewportSize({ width: 1280, height: 800 })
|
||
await host.waitFor({ state: 'detached' })
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 0, 'Abstandhalter blieb stehen')
|
||
eq(await page.evaluate(() => document.documentElement.style.scrollPaddingBottom), '', 'scroll-padding-bottom nicht zurueckgesetzt')
|
||
await page.setViewportSize(HANDY)
|
||
await host.waitFor({ state: 'attached' })
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 1, 'Abstandhalter nach der Rueckkehr')
|
||
}, { viewport: HANDY })
|
||
|
||
await check('kontaktleiste_nicht_auf_dem_desktop', async (page) => {
|
||
await page.goto(srv.seite('desktop.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
eq(await anzahl(page, HOST('kontakt')), 0, 'Kontakt-Leiste auf dem Desktop')
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 0, 'Abstandhalter auf dem Desktop')
|
||
})
|
||
|
||
await check('kontaktleiste_aus_per_team', async (page) => {
|
||
await page.goto(srv.seite('kontakt-aus.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
eq(await anzahl(page, HOST('kontakt')), 0, 'Kontakt-Leiste trotz Team-Schalter')
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 0, 'Abstandhalter trotz Team-Schalter')
|
||
}, { viewport: HANDY, team: { kontaktleisteAus: true } })
|
||
|
||
await check('kontaktleiste_nur_mit_daten', async (page) => {
|
||
await page.goto(srv.seite('kontakt-nur-route.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const host = page.locator(HOST('kontakt'))
|
||
await host.waitFor({ state: 'attached' })
|
||
eq(await host.locator('a').allInnerTexts(), ['Route'], 'nur Route (kein Telefon, kein WhatsApp)')
|
||
}, { viewport: HANDY, eingabe: zeitenEingabe({ kontakt: { an: true, telefon: '', adresse: ADRESSE } }) })
|
||
|
||
await check('kontaktleiste_ohne_daten', async (page) => {
|
||
srv.ueberschreibe((a) => ({ ...a, kontakt: null })) // der Browser hat die Antwort eines frueheren Abrufs sonst 60 Sekunden im Zwischenspeicher
|
||
await page.goto(srv.seite('kontakt-leer.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
eq(await anzahl(page, HOST('kontakt')), 0, 'Leiste ohne Daten')
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 0, 'Abstandhalter ohne Daten')
|
||
}, { viewport: HANDY })
|
||
|
||
await check('eigene_anrufleiste', async (page) => {
|
||
const eigene = '<div id="eigene" style="position:fixed;bottom:0;left:0;right:0;height:56px;background:#fff;border-top:1px solid #ccc;z-index:50"><a href="tel:+496301123456" style="display:block;line-height:56px;text-align:center">Jetzt anrufen</a></div>'
|
||
await page.goto(srv.seite('eigene-leiste.html', seitenHtml({ anfang: eigene })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' }) // das Skript hat gezeichnet ...
|
||
eq(await anzahl(page, HOST('kontakt')), 0, 'Kontakt-Leiste neben einer eigenen Anruf-Leiste')
|
||
eq(await anzahl(page, '[data-wkz-abstand]'), 0, 'Abstandhalter neben einer eigenen Anruf-Leiste')
|
||
}, { viewport: HANDY, eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('anruf_im_kopf_stoert_nicht', async (page) => {
|
||
// ein tel:-Link in einem festen Kopf OBEN ist keine Anruf-Leiste unten
|
||
const kopf = '<header style="position:fixed;top:0;left:0;right:0;height:48px;background:#222;z-index:100"><a href="tel:+496301123456" style="color:#fff;line-height:48px">06301 123456</a></header>'
|
||
await page.goto(srv.seite('anruf-kopf.html', seitenHtml({ anfang: kopf })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('kontakt')).waitFor({ state: 'attached' })
|
||
}, { viewport: HANDY })
|
||
|
||
await check('unsichere_daten_werden_nicht_verwendet', async (page) => {
|
||
srv.ueberschreibe((a) => ({
|
||
...a,
|
||
hinweise: [{ art: 'wichtig', text: '<img src=x onerror="window.__x=1">Achtung', knopf: { art: 'link', ziel: 'javascript:window.__y=1', text: 'Los' }, ab: 0, bis: 8.64e15, rang: 4 }],
|
||
kontakt: { telefon: '491711234567', routeUrl: 'javascript:window.__z=1', whatsappUrl: 'http://wa.me/491711234567' },
|
||
}))
|
||
await page.goto(srv.seite('boese.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const region = page.getByRole('region', { name: 'Hinweis' })
|
||
await region.waitFor()
|
||
assert((await region.innerText()).includes('<img src=x onerror="window.__x=1">Achtung'), 'Text nicht als Text gezeigt')
|
||
eq(await region.locator('img').count(), 0, 'img eingeschleust')
|
||
eq(await region.locator('a').count(), 0, 'javascript:-Knopf wurde ein Link')
|
||
const host = page.locator(HOST('kontakt'))
|
||
await host.waitFor({ state: 'attached' })
|
||
eq(await host.locator('a').allInnerTexts(), ['Anrufen'], 'nur der Anruf-Knopf bleibt (Route: javascript:, WhatsApp: http:)')
|
||
eq(await page.evaluate(() => [window.__x, window.__y, window.__z]), [undefined, undefined, undefined], 'Skript ausgefuehrt')
|
||
}, { viewport: HANDY })
|
||
|
||
await check('jsonld_ergaenzt', async (page) => {
|
||
const bekannt = { '@context': 'https://schema.org', '@type': 'Bakery', name: FIRMA, telephone: '+49 171 1234567', address: ADRESSE, openingHoursSpecification: [{ '@type': 'OpeningHoursSpecification', dayOfWeek: 'Sunday', opens: '08:00', closes: '10:00' }] }
|
||
const web = { '@context': 'https://schema.org', '@type': 'WebSite', name: FIRMA, url: 'https://baeckerei.example/' }
|
||
const kaputt = '{ "@type": "Bakery", kaputt'
|
||
const kopf = `<script type="application/ld+json">${JSON.stringify(web)}</script><script type="application/ld+json">${kaputt}</script><script type="application/ld+json">${JSON.stringify(bekannt)}</script>`
|
||
await page.goto(srv.seite('jsonld.html', seitenHtml({ kopf })))
|
||
await warteBereit(page)
|
||
const texte = await page.evaluate(() => [...document.querySelectorAll('script[type="application/ld+json"]')].map((s) => s.textContent))
|
||
eq(texte.length, 3, 'Anzahl der JSON-LD-Bloecke (kein zweiter Block fuer ein vorhandenes Unternehmen)')
|
||
eq(JSON.parse(texte[0]), web, 'WebSite-Block veraendert')
|
||
eq(texte[1], kaputt, 'unparsbarer Block veraendert')
|
||
const api = await (await fetch(API)).json()
|
||
const neu = JSON.parse(texte[2])
|
||
eq(neu.openingHoursSpecification, api.jsonld.openingHoursSpecification, 'Zeiten nicht aus der Schnittstelle')
|
||
eq(neu.specialOpeningHoursSpecification ?? [], api.jsonld.specialOpeningHoursSpecification, 'Sonderzeiten')
|
||
assert(neu.openingHoursSpecification.length === 6, `Mo-Sa = 6 Eintraege (statt ${neu.openingHoursSpecification.length})`)
|
||
assert(!JSON.stringify(neu).includes('"Sunday"'), 'alter Sonntag-Eintrag blieb stehen')
|
||
eq([neu['@type'], neu.name, neu.telephone, neu.address], ['Bakery', FIRMA, '+49 171 1234567', ADRESSE], 'uebrige Felder des Blocks')
|
||
})
|
||
|
||
await check('jsonld_neuer_block', async (page) => {
|
||
const web = { '@context': 'https://schema.org', '@type': 'WebSite', name: FIRMA }
|
||
await page.goto(srv.seite('jsonld-neu.html', seitenHtml({ kopf: `<script type="application/ld+json">${JSON.stringify(web)}</script>` })))
|
||
await warteBereit(page)
|
||
const texte = await page.evaluate(() => [...document.querySelectorAll('script[type="application/ld+json"]')].map((s) => s.textContent))
|
||
eq(texte.length, 2, 'Anzahl der Bloecke (WebSite + neuer Block)')
|
||
eq(JSON.parse(texte[0]), web, 'WebSite-Block veraendert')
|
||
const api = await (await fetch(API)).json()
|
||
const neu = JSON.parse(texte[1])
|
||
eq(neu['@type'], 'LocalBusiness', '@type')
|
||
eq(neu['@context'], api.jsonld.neuerBlock['@context'], '@context kommt vom Server')
|
||
eq([neu.name, neu.address], [FIRMA, ADRESSE], 'name/address')
|
||
eq(neu.url, `${srv.pageOrigin}/`, 'url')
|
||
eq(neu.openingHoursSpecification, api.jsonld.openingHoursSpecification, 'Zeiten')
|
||
})
|
||
|
||
await check('jsonld_ohne_zeiten_bleibt_unberuehrt', async (page) => {
|
||
srv.ueberschreibe((a) => ({ ...a, jsonld: null }))
|
||
const bekannt = { '@context': 'https://schema.org', '@type': 'Bakery', name: FIRMA, openingHoursSpecification: [{ '@type': 'OpeningHoursSpecification', dayOfWeek: 'Sunday', opens: '08:00', closes: '10:00' }] }
|
||
const text = JSON.stringify(bekannt)
|
||
await page.goto(srv.seite('jsonld-null.html', seitenHtml({ kopf: `<script type="application/ld+json">${text}</script>` })))
|
||
await warteBereit(page)
|
||
const texte = await page.evaluate(() => [...document.querySelectorAll('script[type="application/ld+json"]')].map((s) => s.textContent))
|
||
eq(texte, [text], 'ohne Zeiten bleibt das JSON-LD wie es ist (und es kommt kein Block dazu)')
|
||
})
|
||
|
||
await check('aktiv_false_ersatz_bleibt', async (page) => {
|
||
srv.setzeFunktion('disabled')
|
||
const ld = JSON.stringify({ '@context': 'https://schema.org', '@type': 'Bakery', name: FIRMA, openingHoursSpecification: [] })
|
||
const html = seitenHtml({
|
||
kopf: `<script type="application/ld+json">${ld}</script>`,
|
||
inhalt: `<div id="ersatz" data-webklar-zeiten data-modus="tabelle"><table><caption>Öffnungszeiten</caption><tr><th>Montag</th><td>09:00–18:00</td></tr></table></div>${lang(5)}`,
|
||
})
|
||
await warteAufSchnittstelle(page, () => page.goto(srv.seite('aus.html', html)))
|
||
assert((await page.locator('#ersatz').innerText()).includes('09:00–18:00'), 'Ersatz-HTML ist weg')
|
||
eq(await anzahl(page, '#ersatz table'), 1, 'Ersatz-Tabelle')
|
||
eq(await anzahl(page, '#ersatz .wkz-tabelle'), 0, 'Tabelle des Skripts trotz aktiv: false')
|
||
eq(await anzahl(page, '[data-wkz-leiste]'), 0, 'Leiste trotz aktiv: false')
|
||
eq(await page.evaluate(() => document.querySelector('script[type="application/ld+json"]').textContent), ld, 'JSON-LD veraendert')
|
||
eq(await anzahl(page, 'script[type="application/ld+json"]'), 1, 'neuer JSON-LD-Block trotz aktiv: false')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('spaeter_platzhalter_wird_gefuellt', async (page) => {
|
||
// Einzelseiten-App: der Platzhalter kommt erst nach einem Seitenwechsel auf die Seite (der erste Abruf ist dann laengst durch)
|
||
const html = seitenHtml({ inhalt: `<div id="spa"></div>${lang(5)}`, skript: SKRIPT })
|
||
await page.goto(srv.seite('spa.html', html))
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' }) // das Skript hat gelaufen, es gibt keinen Platzhalter
|
||
await page.evaluate(() => {
|
||
const d = document.createElement('div')
|
||
d.id = 'neu'
|
||
d.setAttribute('data-webklar-zeiten', '')
|
||
d.setAttribute('data-modus', 'status')
|
||
d.textContent = 'Ersatz-Status'
|
||
document.getElementById('spa').appendChild(d)
|
||
})
|
||
await page.waitForFunction(() => { const s = document.getElementById('neu'); return s && !s.textContent.includes('Ersatz') }, null, { timeout: 3000 })
|
||
assert((await page.locator('#neu').innerText()).includes('geöffnet'), `Status: ${await page.locator('#neu').innerText()}`)
|
||
// ein zweiter Seitenwechsel: der alte Knoten geht, ein neuer kommt
|
||
await page.evaluate(() => {
|
||
document.getElementById('neu').remove()
|
||
const d = document.createElement('span')
|
||
d.id = 'neu2'
|
||
d.setAttribute('data-webklar-zeiten', '')
|
||
d.setAttribute('data-modus', 'badge')
|
||
d.textContent = 'Ersatz-Badge'
|
||
document.getElementById('spa').appendChild(d)
|
||
})
|
||
await page.waitForFunction(() => { const s = document.getElementById('neu2'); return s && !s.textContent.includes('Ersatz') }, null, { timeout: 3000 })
|
||
eq(await page.locator('#neu2').innerText(), 'Geöffnet', 'Badge')
|
||
eq(await anzahl(page, 'style#wkz-style'), 1, 'Stile')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('wieder_eingehaengter_platzhalter_wird_neu_gezeichnet', async (page) => {
|
||
await page.goto(srv.seite('spa-zurueck.html', seitenHtml({ inhalt: `<div id="spa"></div>${STATUS}${lang(5)}` })))
|
||
await warteBereit(page)
|
||
// der Knoten verlaesst die Seite, ein anderer Platzhalter loest ein Zeichnen aus (der alte wird dabei vergessen) ...
|
||
await page.evaluate(() => { window.__alt = document.getElementById('status'); window.__alt.remove() })
|
||
await page.evaluate(() => {
|
||
const d = document.createElement('div')
|
||
d.id = 'anderer'
|
||
d.setAttribute('data-webklar-zeiten', '')
|
||
d.setAttribute('data-modus', 'status')
|
||
d.textContent = 'Ersatz-Status'
|
||
document.getElementById('spa').appendChild(d)
|
||
})
|
||
await page.waitForFunction(() => { const s = document.getElementById('anderer'); return s && !s.textContent.includes('Ersatz') }, null, { timeout: 3000 })
|
||
// ... und kommt zurueck, mit dem Inhalt, den die App ihm gegeben hat
|
||
await page.evaluate(() => { window.__alt.textContent = 'Ersatz-Status'; document.getElementById('spa').appendChild(window.__alt) })
|
||
await page.waitForFunction(() => !document.getElementById('status').textContent.includes('Ersatz'), null, { timeout: 3000 })
|
||
assert((await statusText(page)).includes('geöffnet'), `Status: ${await statusText(page)}`)
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('unbekannter_plan', async (page) => {
|
||
const meldungen = []
|
||
page.on('console', (m) => { if (m.type() === 'warning') meldungen.push(m.text()) })
|
||
const html = seitenHtml({ inhalt: `<div id="ersatz" data-webklar-zeiten data-modus="status" data-plan="gibtsnicht">Ersatz-Status</div>${lang(5)}` })
|
||
await page.goto(srv.seite('plan.html', html))
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' }) // die Leisten haengen nicht am Plan
|
||
eq(await page.locator('#ersatz').innerText(), 'Ersatz-Status', 'Ersatz-HTML bei unbekanntem Plan')
|
||
assert(meldungen.some((m) => m.includes('gibtsnicht')), `Konsole-Warnung fehlt: ${JSON.stringify(meldungen)}`)
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('kein_storage', async (page) => {
|
||
await page.goto(srv.seite('storage.html', seitenHtml({ anfang: KOPF_FEST })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
await page.locator(HOST('kontakt')).waitFor({ state: 'attached' })
|
||
const s = await page.evaluate(async () => ({ c: document.cookie, l: localStorage.length, s: sessionStorage.length, db: (await indexedDB.databases()).length }))
|
||
eq(s, { c: '', l: 0, s: 0, db: 0 }, 'Cookies/Storage')
|
||
eq(await page.context().cookies(), [], 'Cookies im Browser')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), viewport: HANDY })
|
||
|
||
await check('doppelt_geladen', async (page) => {
|
||
const anfragen = []
|
||
page.on('request', (r) => { if (r.url().includes('/api/public/zeiten/v1/')) anfragen.push(r.url()) })
|
||
await page.goto(srv.seite('doppelt.html', seitenHtml({ skript: `${SKRIPT}${SKRIPT}` })))
|
||
await warteBereit(page)
|
||
await page.waitForTimeout(300)
|
||
eq(await anzahl(page, HOST('hinweis')), 1, 'Hinweis-Leisten')
|
||
eq(await anzahl(page, 'style#wkz-style'), 1, 'Stile')
|
||
eq(anfragen.length, 1, 'Abrufe der Schnittstelle')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }) })
|
||
|
||
await check('reduzierte_bewegung', async (page) => {
|
||
await page.goto(srv.seite('ruhig.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const region = page.getByRole('region', { name: 'Hinweis' })
|
||
await region.waitFor()
|
||
eq(await region.evaluate((e) => getComputedStyle(e).animationName), 'none', 'Animation trotz prefers-reduced-motion')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), context: { reducedMotion: 'reduce' } })
|
||
|
||
await check('normale_bewegung_hat_einblendung', async (page) => {
|
||
await page.goto(srv.seite('bewegt.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
const region = page.getByRole('region', { name: 'Hinweis' })
|
||
await region.waitFor()
|
||
assert((await region.evaluate((e) => getComputedStyle(e).animationName)) !== 'none', 'ohne Praeferenz soll die Leiste einblenden')
|
||
}, { eingabe: zeitenEingabe({ hinweise: [INFO] }), context: { reducedMotion: 'no-preference' } })
|
||
|
||
await check('status_wechselt_um_18_uhr', async (page, ctx) => {
|
||
await page.goto(srv.seite('status.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
eq(await statusText(page), STATUS_OFFEN, 'Status um 17:55')
|
||
await ctx.clock.fastForward(6 * 60 * 1000) // 18:01
|
||
await page.waitForFunction((text) => document.getElementById('status').textContent.trim() === text, STATUS_ZU)
|
||
eq(await statusText(page), STATUS_ZU, 'Status nach 18 Uhr')
|
||
}, { uhr: '2026-10-05T15:55:00.000Z' })
|
||
|
||
await check('neu_laden_nach_zehn_minuten', async (page, ctx) => {
|
||
const anfragen = []
|
||
page.on('request', (r) => { if (r.url().includes('/api/public/zeiten/v1/')) anfragen.push(r.url()) })
|
||
await page.goto(srv.seite('neu-laden.html', seitenHtml()))
|
||
await warteBereit(page)
|
||
eq(anfragen.length, 1, 'ein Abruf beim Laden')
|
||
eq(await anzahl(page, HOST('hinweis')), 0, 'noch kein Hinweis')
|
||
// das Team stellt einen Hinweis ein; die Server-Uhr steht auf dem neuen Zeitpunkt
|
||
await srv.speichere(zeitenEingabe({ hinweise: [INFO] }))
|
||
srv.setzeUhr('2026-10-05T16:06:00.000Z')
|
||
await ctx.clock.fastForward(11 * 60 * 1000)
|
||
await page.locator(HOST('hinweis')).waitFor({ state: 'attached' })
|
||
assert(anfragen.length === 2, `zweiter Abruf nach 10 Minuten (Abrufe: ${anfragen.length})`)
|
||
}, { uhr: '2026-10-05T15:55:00.000Z' })
|
||
|
||
await check('fetch_wirft_sofort', async (page) => {
|
||
const html = seitenHtml({ inhalt: `<div id="ersatz" data-webklar-zeiten data-modus="status">Ersatz-Status</div>${lang(3)}` })
|
||
await page.goto(srv.seite('fetch-wirft.html', html))
|
||
await page.waitForTimeout(500)
|
||
eq(await page.locator('#ersatz').innerText(), 'Ersatz-Status', 'Ersatz-HTML')
|
||
eq(await anzahl(page, '[data-wkz-leiste]'), 0, 'Leiste ohne Daten')
|
||
}, { initScript: () => { window.fetch = () => { throw new TypeError('fetch kaputt') } } })
|
||
|
||
await check('screenshots_handy', async (page) => {
|
||
await page.goto(srv.seite('shot.html', seitenHtml({ anfang: KOPF_FEST, inhalt: `<div style="height:60px"></div>${STATUS}${lang(12)}` })))
|
||
await warteBereit(page)
|
||
await page.locator(HOST('kontakt')).waitFor({ state: 'attached' })
|
||
await page.waitForTimeout(400)
|
||
await shot(page, 'handy-hinweis-und-kontakt')
|
||
}, { viewport: HANDY, eingabe: zeitenEingabe({ hinweise: [hinweis('urlaub', 'Betriebsurlaub vom 12. bis 16. Oktober – ab dem 17. sind wir wieder für Euch da.', { knopf: { art: 'anruf', ziel: '06301 1234', text: 'Anrufen' } })] }) })
|
||
}
|
||
|
||
browser = await chromium.launch()
|
||
try {
|
||
await pruefungen()
|
||
} finally {
|
||
await browser.close()
|
||
await srv.stop()
|
||
}
|
||
|
||
// Nur der Testserver (und kein Dritter) wurde angefragt
|
||
const erlaubt = new Set([new URL(srv.url).host, new URL(srv.pageOrigin).host])
|
||
const fremd = [...hosts].filter((h) => !erlaubt.has(h))
|
||
if (fremd.length) {
|
||
results.push(['kein_fremder_host', false])
|
||
console.log(`FAIL kein_fremder_host: ${fremd.join(', ')}`)
|
||
} else {
|
||
results.push(['kein_fremder_host', true])
|
||
console.log('ok kein_fremder_host')
|
||
}
|
||
|
||
const failed = results.filter(([, ok]) => !ok)
|
||
console.log(`${results.length - failed.length}/${results.length} Pruefungen bestanden`)
|
||
process.exit(failed.length ? 1 : 0)
|