Workers P6: Prüfstand für den Reiter Bot-Leads (Desktop und Handy)
Skript-API: botleads({ room, q, page, id }) antwortet wie GET /botleads des Dienstes
(items, total, pageSize 50; Telefon nie bei Heilberufen, gekürzte Stubs nur mit id).
60 sichtbare Einträge in vier Räumen (zwei Seiten), 3 gekürzte Stubs, Heilberufe mit
Altbestand-Telefon, eine http-Quelle, zwei feindliche Quellen, lange Namen und Gründe.
Schalter: faults.botleads, botleadsDelayMs, botleadsEmpty (?botleads=leer), botleadCalls.
Fetch-Weiche und entry.jsx (?bot=<Id>) um die Route und den Deep-Link ergänzt.
Szenarien: botleads (Reiter, Liste, Raumfilter, Blättern mit Fokus, Suche, Leerzustände,
Fehlertexte, Deep-Link, Quellen-Links, Telefon) und handy-botleads (390 x 844: kein
seitliches Scrollen, Knopfhöhe, Kontrast der gekürzten Zeile, Wortumbruch).
Der Pin in workersHarness.test.mjs prüft nun die neue Antwortform statt der leeren Liste.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd532e0cf0
commit
78c8e3fbc2
5 files changed
+709
-11
No files matched your search
@@ -7,7 +7,8 @@
|
||||
// Szenarien: A Desktop, B Blasen, C Handy, D weniger Bewegung, E Notbremse, F Ausfall (P2); G Freigabe, H Interessenten-Panel, I Anhalten
|
||||
// mit Rückfrage, J Handy mit Freigabe und Panel (P3); K Antworten als Mensch, L Vorschau freigeben, M Antwort des Kunden einfügen, N Vorschau
|
||||
// bauen, O Fokus, P Handy für alle Felder (P4, eigener Datensatz ?p4=1, Bilder mit Vorsilbe antworten-, vorschau-, einfuegen-, bauen-, fokus-,
|
||||
// handy-p4-). Jedes Szenario läuft für sich: bricht eines ab, laufen die übrigen weiter und der
|
||||
// handy-p4-); Q Bot-Leads am Desktop und R Bot-Leads am Handy (P6, eigene Daten der Skript-API, Bilder mit Vorsilbe botleads-, handy-botleads-).
|
||||
// Jedes Szenario läuft für sich: bricht eines ab, laufen die übrigen weiter und der
|
||||
// Abbruch steht als Verstoß in der Liste. HARNESS_ONLY=freigabe,interessent (Namen siehe `SCENARIOS` unten) lässt nur diese laufen (für
|
||||
// die Arbeit am Prüfstand; die Abnahme läuft immer vollständig).
|
||||
'use strict'
|
||||
@@ -1684,10 +1685,490 @@ async function handyP4(browser, base) {
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- P6: Bot-Leads (Reiter „Bot-Leads“: Desktop 1280 x 900 und Handy 390 x 844) ----------
|
||||
// Daten der Skript-API (scripted-api.js, „Bot-Leads (P6)“): 60 sichtbare Einträge (je 15 in den vier Räumen; Seite 1 hat 50, Seite 2 hat 10), drei gekürzte
|
||||
// Stubs (nur mit ?bot=<Id> zu sehen), Heilberufe mit Altbestand-Telefon im Speicher, eine http-Quelle und zwei feindliche Quellen (javascript:, Zugangsdaten).
|
||||
// Die Seite läuft über den echten API-Client und die Fetch-Weiche (fault=none): Query, Fehlertexte und Zeitgrenzen sind die des Betriebs. Was die Oberfläche
|
||||
// angefragt hat, steht in window.__wk.api.botleadCalls. Die Kennungen unten sind die aus BOTLEAD_IDS in scripted-api.js.
|
||||
const BL_URL = '/index.html?page=1&fault=none&tab=botleads'
|
||||
const BL_IDS = { health: 'bl-aerzte-01', cut: 'bl-gekuerzt-01', cutHealth: 'bl-gekuerzt-03' } // Heilberuf (Seite 1), gekürzter Handwerk-Stub, gekürzter Heilberuf-Stub
|
||||
const BL_NAMES = { health: 'Praxis Dr. Lindner', physio: 'Physiotherapie Moser', http: 'Elektro Hartmann GmbH', javascript: 'Restaurant Zum Hirschen', credentials: 'Fotografie Lichtblick' }
|
||||
const BL_HEADERS = ['Betrieb', 'Raum', 'Punkte', 'Grund', 'Telefon', 'Quelle', 'Gefunden']
|
||||
const BL_ROOM_LABELS = ['Alle Räume', 'Ärzte & Praxen', 'Handwerk', 'Selbstständige', 'Läden & Gastro']
|
||||
const BL_HEALTH_PHONE = 'bewusst nicht gespeichert'
|
||||
const BL_NO_PHONE = 'nicht verfügbar'
|
||||
const BL_HINT = 'Reine Liste: keine Anrufe, kein Mailversand.'
|
||||
const BL_SESSION = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
||||
const BL_LABEL = (page, pages, count) => `Seite ${page} von ${pages} · ${count} ${count === 1 ? 'Eintrag' : 'Einträge'}`
|
||||
|
||||
const blRows = (page) => page.locator('.wk-bl-table tbody tr')
|
||||
const blCalls = (page) => page.evaluate(() => window.__wk.api.botleadCalls)
|
||||
const blFootLabel = (page) => page.evaluate(() => { const el = document.querySelector('.wk-bl .table-foot > span'); return el ? el.textContent : null })
|
||||
const blWaitLabel = (page, label) => page.waitForFunction((want) => { const el = document.querySelector('.wk-bl .table-foot > span'); return Boolean(el) && el.textContent === want }, label)
|
||||
// Fertig geladen: kein „Wird geladen …“ mehr und Tabelle, Leerzustand oder Fehlerzeile da
|
||||
const blSettled = (page) => page.waitForFunction(() => {
|
||||
const root = document.querySelector('.wk-bl')
|
||||
return Boolean(root) && !root.querySelector('.loading') && Boolean(root.querySelector('table, .empty, .msg.err'))
|
||||
})
|
||||
const blRead = (page) => page.evaluate(() => Array.from(document.querySelectorAll('.wk-bl-table tbody tr')).map((tr) => {
|
||||
const c = tr.children
|
||||
const title = c[0].querySelector('.cell-title')
|
||||
const a = c[5].querySelector('a')
|
||||
return {
|
||||
title: title.childNodes[0] ? title.childNodes[0].textContent : '',
|
||||
tag: title.querySelector('.wk-bl-tag') ? title.querySelector('.wk-bl-tag').textContent : null,
|
||||
place: c[0].querySelector('.cell-sub') ? c[0].querySelector('.cell-sub').textContent : '',
|
||||
room: c[1].textContent.trim(),
|
||||
score: c[2].textContent.trim(),
|
||||
reason: c[3].textContent.trim(),
|
||||
phone: c[4].textContent.trim(),
|
||||
source: c[5].textContent.trim(),
|
||||
found: c[6].textContent.trim(),
|
||||
link: a ? { href: a.getAttribute('href'), rel: a.getAttribute('rel'), target: a.getAttribute('target'), label: a.getAttribute('aria-label') } : null,
|
||||
cut: tr.classList.contains('wk-bl-cut'),
|
||||
current: tr.getAttribute('aria-current'),
|
||||
}
|
||||
}))
|
||||
const blFocus = (page) => page.evaluate(() => {
|
||||
const el = document.activeElement
|
||||
return { tag: el ? el.tagName : null, text: el ? (el.textContent || '').trim().slice(0, 30) : null, inPager: Boolean(el && el.closest && el.closest('.wk-bl-pager')), onBody: el === document.body }
|
||||
})
|
||||
const blDay = (text) => { const m = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text); return m ? Date.UTC(Number(m[3]), Number(m[2]) - 1, Number(m[1])) : NaN }
|
||||
const blTitle = (rows, title) => rows.find((r) => r.title === title)
|
||||
const blSelectRoom = async (page, value) => { await page.getByLabel('Raum', { exact: true }).selectOption(value); await blSettled(page) }
|
||||
|
||||
// Kontrast aller Texte im Bereich scope: Vordergrund und der wirksame Hintergrund (Ebenen der Vorfahren übereinander, auf Weiß) aus den berechneten Stilen,
|
||||
// Deckkraft des Elements und seiner Vorfahren im Vordergrund; Verhältnis nach WCAG 2.x. Läuft im Browser.
|
||||
function contrastProbe(scope) {
|
||||
const parse = (css) => {
|
||||
let m = /^rgba?\(([^)]+)\)$/.exec(css)
|
||||
let scale = 1
|
||||
if (!m) { m = /^color\(srgb ([^)]+)\)$/.exec(css); scale = 255 }
|
||||
if (!m) return null
|
||||
const p = m[1].split(/[\s,/]+/).filter(Boolean).map((v) => (v.endsWith('%') ? parseFloat(v) / 100 : parseFloat(v)))
|
||||
return { r: p[0] * scale, g: p[1] * scale, b: p[2] * scale, a: p.length > 3 ? p[3] : 1 }
|
||||
}
|
||||
const over = (top, bottom) => ({ r: top.r * top.a + bottom.r * (1 - top.a), g: top.g * top.a + bottom.g * (1 - top.a), b: top.b * top.a + bottom.b * (1 - top.a), a: 1 })
|
||||
const lum = (c) => {
|
||||
const lin = (v) => { const s = v / 255; return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4 }
|
||||
return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b)
|
||||
}
|
||||
const out = []
|
||||
const root = document.querySelector(scope)
|
||||
if (!root) return { error: 'kein Bereich ' + scope, items: out }
|
||||
for (const el of [root, ...root.querySelectorAll('*')]) {
|
||||
const own = Array.from(el.childNodes).filter((n) => n.nodeType === 3 && n.textContent.trim() !== '').map((n) => n.textContent.trim()).join(' ')
|
||||
if (own === '') continue
|
||||
const layers = []
|
||||
let opacity = 1
|
||||
for (let n = el; n && n.nodeType === 1; n = n.parentElement) {
|
||||
const cs = getComputedStyle(n)
|
||||
const bg = parse(cs.backgroundColor)
|
||||
if (bg === null) return { error: 'Hintergrund nicht lesbar: ' + cs.backgroundColor, items: out }
|
||||
if (bg.a > 0) layers.push(bg)
|
||||
opacity *= Number(cs.opacity)
|
||||
}
|
||||
let back = { r: 255, g: 255, b: 255, a: 1 }
|
||||
for (const layer of layers.reverse()) back = over(layer, back)
|
||||
const fgRaw = parse(getComputedStyle(el).color)
|
||||
if (fgRaw === null) return { error: 'Farbe nicht lesbar: ' + getComputedStyle(el).color, items: out }
|
||||
const fg = over({ ...fgRaw, a: fgRaw.a * opacity }, back)
|
||||
const l1 = lum(fg)
|
||||
const l2 = lum(back)
|
||||
out.push({ text: own.slice(0, 40), tag: el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(/\s+/)[0] : ''), ratio: Math.round(((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)) * 100) / 100, color: getComputedStyle(el).color, background: 'rgb(' + [back.r, back.g, back.b].map(Math.round).join(', ') + ')' })
|
||||
}
|
||||
return { error: null, items: out }
|
||||
}
|
||||
const blContrast = async (page, scope) => {
|
||||
const report = await page.evaluate(contrastProbe, scope)
|
||||
const worst = [...report.items].sort((a, b) => a.ratio - b.ratio)
|
||||
return { error: report.error, count: report.items.length, min: worst.length ? worst[0].ratio : null, low: report.items.filter((i) => i.ratio < 4.5).map((i) => `${i.tag} „${i.text}“ ${i.ratio}:1 (${i.color} auf ${i.background})`) }
|
||||
}
|
||||
|
||||
// Wörter, die ein Zeilenumbruch mitten im Wort trennt (z. B. „Kaiserslauter“ + „n“): Wörter ohne Bindestrich, Schrägstrich, Punkt, Komma und Klammern, die der
|
||||
// Browser über zwei Zeilen setzt (ein Bereich um das Wort hat dann mehr als ein Rechteck). Läuft im Browser über alle Textknoten im Bereich scope.
|
||||
const blBrokenWords = (page, scope) => page.evaluate((selector) => {
|
||||
const root = document.querySelector(selector)
|
||||
const out = []
|
||||
if (!root) return { error: 'kein Bereich ' + selector, words: out, count: 0 }
|
||||
let count = 0
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
const text = node.textContent
|
||||
for (const match of text.matchAll(/\S+/g)) {
|
||||
const word = match[0]
|
||||
if (word.length < 3 || /[-–/@.,;:()[\]]/.test(word)) continue
|
||||
count += 1
|
||||
const range = document.createRange()
|
||||
range.setStart(node, match.index)
|
||||
range.setEnd(node, match.index + word.length)
|
||||
if (range.getClientRects().length > 1) out.push(word)
|
||||
}
|
||||
}
|
||||
return { error: null, words: out, count }
|
||||
}, scope)
|
||||
|
||||
// ---------- Szenario Q: Bot-Leads (Desktop 1280 x 900) ----------
|
||||
|
||||
async function botleads(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'botleads', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
// 1. Der Reiter: sichtbar, nicht gewählt, mit Klick gewählt
|
||||
await page.goto(base + '/index.html?page=1&fault=none')
|
||||
await page.getByRole('tab').first().waitFor()
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-wk-worker]').length === 19)
|
||||
const tab = page.getByRole('tab', { name: 'Bot-Leads', exact: true })
|
||||
check('botleads: Reiter „Bot-Leads“ ist sichtbar und vor dem Klick nicht gewählt', (await tab.isVisible()) && (await tab.getAttribute('aria-selected')) === 'false')
|
||||
await tab.click()
|
||||
await blSettled(page)
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
check('botleads: nach dem Klick ist „Bot-Leads“ gewählt, das Hochhaus ist weg und die Liste da', (await tab.getAttribute('aria-selected')) === 'true' && (await page.locator('[data-wk-worker]').count()) === 0 && (await blRows(page).count()) > 0)
|
||||
|
||||
// 2. Liste, Seite 1
|
||||
const heads = (await page.locator('.wk-bl-table thead th').allTextContents()).map((t) => t.trim())
|
||||
check('botleads: Spalten Betrieb, Raum, Punkte, Grund, Telefon, Quelle, Gefunden', same(heads, BL_HEADERS), heads)
|
||||
const page1 = await blRead(page)
|
||||
check('botleads: Seite 1 zeigt 50 Zeilen, die Seitenzeile „Seite 1 von 2 · 60 Einträge“', page1.length === 50 && (await blFootLabel(page)) === BL_LABEL(1, 2, 60), { rows: page1.length, label: await blFootLabel(page) })
|
||||
check('botleads: Hinweis „Reine Liste: keine Anrufe, kein Mailversand.“ steht über der Liste', (await textOf(page.locator('.wk-bl > .wk-hint'))) === BL_HINT)
|
||||
const pagerState = async () => ({
|
||||
prev: await page.getByRole('button', { name: 'Zurück', exact: true }).isDisabled(),
|
||||
next: await page.getByRole('button', { name: 'Weiter', exact: true }).isDisabled(),
|
||||
})
|
||||
check('botleads: auf Seite 1 ist „Zurück“ gesperrt und „Weiter“ frei', same(await pagerState(), { prev: true, next: false }), await pagerState())
|
||||
const days1 = page1.map((r) => blDay(r.found))
|
||||
check('botleads: neueste zuerst (Datum TT.MM.JJJJ, nie aufsteigend), erste Zeile ist der jüngste Fund', days1.every((d, i) => Number.isFinite(d) && (i === 0 || days1[i - 1] >= d)) && page1[0].title === BL_NAMES.health, { first: page1[0] })
|
||||
const bad = page1.filter((r) => /undefined|NaN|\[object|null/.test(Object.values(r).filter((v) => typeof v === 'string').join(' ')))
|
||||
check('botleads: kein „undefined“, „NaN“ oder „null“ in den Zeilen', bad.length === 0, bad.slice(0, 2))
|
||||
check('botleads: gekürzte Einträge fehlen in der Liste (kein „[gekürzt]“, kein Kürzel, nicht in der Zählung)', page1.every((r) => r.title !== '[gekürzt]' && r.tag === null && !r.cut))
|
||||
const options = await page.getByLabel('Raum', { exact: true }).locator('option').allTextContents()
|
||||
check('botleads: Raumfilter bietet alle Räume und die vier Räume', same(options, BL_ROOM_LABELS), options)
|
||||
const settledCalls = (await blCalls(page)).length
|
||||
await sleep(1300)
|
||||
check('botleads: kein Polling (nach 1,3 s keine weitere Abfrage)', (await blCalls(page)).length === settledCalls, settledCalls)
|
||||
const callFirst = (await blCalls(page))[0]
|
||||
check('botleads: die erste Abfrage ist Raum leer, Suche leer, Seite 1', same(callFirst, { room: '', q: '', page: 1, id: '' }), callFirst)
|
||||
const w0 = await page.evaluate(() => ({ scroll: document.scrollingElement.scrollWidth, inner: window.innerWidth }))
|
||||
check('botleads: Liste ohne seitliches Scrollen am Desktop', w0.scroll <= w0.inner, w0)
|
||||
const broken0 = await blBrokenWords(page, '.wk-bl-table tbody')
|
||||
check('botleads: am Desktop wird kein Wort mitten im Wort umgebrochen', broken0.error === null && broken0.count > 100 && broken0.words.length === 0, broken0)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'botleads-liste.png')
|
||||
await shoot(page, 'botleads-liste-ganz.png', { fullPage: true })
|
||||
|
||||
// 3. Telefon: Heilberufe nie, sonst nur mit Quelle
|
||||
const healthRows = page1.filter((r) => r.room === 'Ärzte & Praxen')
|
||||
check('botleads: jede Zeile im Raum „Ärzte & Praxen“ zeigt „bewusst nicht gespeichert“ statt einer Nummer', healthRows.length >= 10 && healthRows.every((r) => r.phone === BL_HEALTH_PHONE), healthRows.map((r) => r.phone))
|
||||
const physio = blTitle(page1, BL_NAMES.physio)
|
||||
check('botleads: auch die Physiotherapie im Raum „Selbstständige“ (Heilberuf) zeigt keine Nummer', Boolean(physio) && physio.phone === BL_HEALTH_PHONE, physio)
|
||||
const storedHealthPhones = await page.evaluate(() => window.__wk.api.botleadsHealthPhones)
|
||||
const bodyDigits = await page.evaluate(() => document.body.textContent.replace(/\s+/g, ' '))
|
||||
check('botleads: keine der im Altbestand gespeicherten Heilberuf-Nummern steht irgendwo auf der Seite', storedHealthPhones.length >= 5 && storedHealthPhones.every((n) => !bodyDigits.includes(n) && !bodyDigits.replace(/\s/g, '').includes(n.replace(/\s/g, ''))), storedHealthPhones)
|
||||
const others = page1.filter((r) => r.room !== 'Ärzte & Praxen' && r.title !== BL_NAMES.physio)
|
||||
check('botleads: Betriebe außerhalb der Heilberufe zeigen ihre Nummer, wenn eine Quelle da ist, sonst „nicht verfügbar“', others.some((r) => PHONE_NUMBER.test(r.phone)) && others.some((r) => r.phone === BL_NO_PHONE) && others.every((r) => PHONE_NUMBER.test(r.phone) || r.phone === BL_NO_PHONE), others.map((r) => r.phone))
|
||||
const healthRow = blRows(page).filter({ has: page.locator('.cell-title', { hasText: BL_NAMES.health }) })
|
||||
await healthRow.screenshot({ path: path.join(out, 'botleads-heilberuf-ohne-telefon.png') })
|
||||
shots.push(path.join(out, 'botleads-heilberuf-ohne-telefon.png'))
|
||||
|
||||
// 4. Quellen-Links
|
||||
const links = page1.filter((r) => r.link)
|
||||
const linkProblems = links.filter((r) => {
|
||||
let url
|
||||
try { url = new URL(r.link.href) } catch { return true }
|
||||
const tokens = (r.link.rel || '').split(/\s+/)
|
||||
return !(url.protocol === 'https:' || url.protocol === 'http:') || url.username !== '' || url.password !== '' || r.link.target !== '_blank' || !tokens.includes('noopener') || !tokens.includes('noreferrer') || !(r.link.label || '').startsWith('Quelle öffnen: ') || r.source !== 'Quelle öffnen'
|
||||
})
|
||||
check('botleads: jeder Quellen-Link ist http(s) ohne Zugangsdaten, öffnet im neuen Tab mit rel „noopener noreferrer“ und hat einen eigenen Namen', links.length >= 40 && linkProblems.length === 0, linkProblems.slice(0, 3))
|
||||
const httpRow = blTitle(page1, BL_NAMES.http)
|
||||
check('botleads: die http-Quelle bleibt ein Link mit rel noopener (die Ansicht lässt http und https zu)', Boolean(httpRow && httpRow.link) && httpRow.link.href.startsWith('http://') && (httpRow.link.rel || '').includes('noopener'), httpRow && httpRow.link)
|
||||
const hostile = [blTitle(page1, BL_NAMES.javascript), blTitle(page1, BL_NAMES.credentials)]
|
||||
check('botleads: feindliche Quellen (javascript:, Zugangsdaten in der Adresse) werden kein Link, die Zelle zeigt „–“', hostile.every((r) => r && r.link === null && r.source === '–'), hostile)
|
||||
check('botleads: auf der Seite steht kein Link mit javascript: oder data:', (await page.locator('a[href^="javascript:" i], a[href^="data:" i]').count()) === 0)
|
||||
|
||||
// 5. Raumfilter
|
||||
await blSelectRoom(page, 'handwerk')
|
||||
const hw = await blRead(page)
|
||||
check('botleads: Raum „Handwerk“ zeigt genau die 15 Handwerk-Zeilen, die Seitenzeile ohne Blättern', hw.length === 15 && hw.every((r) => r.room === 'Handwerk') && (await blFootLabel(page)) === BL_LABEL(1, 1, 15) && (await page.locator('.wk-bl-pager').count()) === 0, { rows: hw.length, label: await blFootLabel(page) })
|
||||
const callHw = (await blCalls(page)).at(-1)
|
||||
check('botleads: der Filter fragt den Dienst mit room=handwerk, Seite 1', callHw.room === 'handwerk' && callHw.page === 1 && callHw.q === '', callHw)
|
||||
const longRow = blTitle(hw, 'Elektroinstallation-und-Haustechnik-Hoffmann-Sonnenschein-Wittgensteiner GmbH')
|
||||
check('botleads: der lange Name und der lange Grund erscheinen vollständig', Boolean(longRow) && longRow.reason.startsWith('Impressum nennt nur eine Postfachadresse') && longRow.reason.endsWith('gehört einem Dritten.'), longRow)
|
||||
await shoot(page, 'botleads-raum-handwerk.png')
|
||||
await blSelectRoom(page, 'aerzte')
|
||||
const ae = await blRead(page)
|
||||
check('botleads: Raum „Ärzte & Praxen“ zeigt 15 Praxen, alle ohne Nummer', ae.length === 15 && ae.every((r) => r.room === 'Ärzte & Praxen' && r.phone === BL_HEALTH_PHONE), ae.map((r) => r.phone))
|
||||
await shoot(page, 'botleads-raum-aerzte.png')
|
||||
await blSelectRoom(page, '')
|
||||
check('botleads: „Alle Räume“ bringt die 50 Zeilen von Seite 1 zurück', (await blRows(page).count()) === 50 && (await blFootLabel(page)) === BL_LABEL(1, 2, 60))
|
||||
|
||||
// 6. Blättern mit der Tastatur: der Fokus bleibt auf der Seitenzeile
|
||||
const next = page.getByRole('button', { name: 'Weiter', exact: true })
|
||||
const prev = page.getByRole('button', { name: 'Zurück', exact: true })
|
||||
await next.focus()
|
||||
check('botleads: „Weiter“ lässt sich per Tastatur fokussieren', (await blFocus(page)).inPager)
|
||||
await page.keyboard.press('Enter')
|
||||
await blWaitLabel(page, BL_LABEL(2, 2, 60))
|
||||
await blSettled(page)
|
||||
const page2 = await blRead(page)
|
||||
const focusNext = await blFocus(page)
|
||||
check('botleads: nach „Weiter“ (Enter) bleibt der Tastaturfokus auf der Seitenzeile (Knopf in .wk-bl-pager)', focusNext.inPager, focusNext)
|
||||
check('botleads: Seite 2 zeigt die letzten 10 Einträge, „Zurück“ frei und „Weiter“ gesperrt', page2.length === 10 && same(await pagerState(), { prev: false, next: true }), { rows: page2.length, state: await pagerState() })
|
||||
const allTitles = [...page1, ...page2].map((r) => r.title)
|
||||
check('botleads: beide Seiten zusammen sind 60 verschiedene Betriebe, Seite 2 knüpft zeitlich an Seite 1 an', new Set(allTitles).size === 60 && blDay(page2[0].found) <= blDay(page1.at(-1).found), { unique: new Set(allTitles).size })
|
||||
const call2 = (await blCalls(page)).at(-1)
|
||||
check('botleads: „Weiter“ fragt Seite 2 mit demselben Filter', call2.page === 2 && call2.room === '' && call2.q === '', call2)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'botleads-seite-2.png')
|
||||
await prev.focus()
|
||||
await page.keyboard.press('Enter')
|
||||
await blWaitLabel(page, BL_LABEL(1, 2, 60))
|
||||
await blSettled(page)
|
||||
const focusPrev = await blFocus(page)
|
||||
check('botleads: nach „Zurück“ (Enter) bleibt der Tastaturfokus auf der Seitenzeile', focusPrev.inPager, focusPrev)
|
||||
check('botleads: „Zurück“ bringt Seite 1 mit 50 Zeilen', (await blRows(page).count()) === 50 && same((await blRead(page)).map((r) => r.title), page1.map((r) => r.title)))
|
||||
metrics.botleadsFocusAfterPaging = { forward: focusNext, back: focusPrev }
|
||||
|
||||
// 7. Wechsel des Raums auf Seite 2 springt auf Seite 1; Ladezustand und späte Antworten
|
||||
await next.click()
|
||||
await blWaitLabel(page, BL_LABEL(2, 2, 60))
|
||||
await blSelectRoom(page, 'laden')
|
||||
const callLaden = (await blCalls(page)).at(-1)
|
||||
check('botleads: Raumwechsel auf Seite 2 fragt wieder Seite 1', callLaden.room === 'laden' && callLaden.page === 1, callLaden)
|
||||
await blSelectRoom(page, '')
|
||||
await page.evaluate(() => { window.__wk.api.botleadsDelayMs = (query) => (query.room === 'handwerk' ? 1000 : 0) })
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('handwerk')
|
||||
const loadingText = await textOf(page.locator('.wk-bl .loading'))
|
||||
check('botleads: während der Antwort steht „Wird geladen …“ (role=status), keine alte Tabelle', loadingText === 'Wird geladen …' && (await page.locator('.wk-bl .loading').getAttribute('role')) === 'status' && (await page.locator('.wk-bl-table').count()) === 0, loadingText)
|
||||
await shoot(page, 'botleads-laedt.png')
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('laden')
|
||||
await blSettled(page)
|
||||
const lad = await blRead(page)
|
||||
await sleep(1500)
|
||||
const ladLater = await blRead(page)
|
||||
check('botleads: die späte Antwort des früheren Filters (Handwerk) überschreibt die neue Liste (Läden) nicht', lad.length === 15 && same(ladLater, lad) && ladLater.every((r) => r.room === 'Läden & Gastro') && (await page.getByLabel('Raum', { exact: true }).inputValue()) === 'laden', { rows: ladLater.length })
|
||||
await page.evaluate(() => { window.__wk.api.botleadsDelayMs = 0 })
|
||||
await blSelectRoom(page, '')
|
||||
|
||||
// 8. Suche und Leerzustand mit Filter
|
||||
const search = page.getByLabel('Betriebe suchen', { exact: true })
|
||||
await search.fill('Hartmann')
|
||||
await page.waitForFunction(() => window.__wk.api.botleadCalls.some((c) => c.q === 'Hartmann'))
|
||||
await blSettled(page)
|
||||
const hartmann = await blRead(page)
|
||||
check('botleads: Suche „Hartmann“ findet genau einen Betrieb, Seitenzeile „1 Eintrag“', hartmann.length === 1 && hartmann[0].title === BL_NAMES.http && (await blFootLabel(page)) === BL_LABEL(1, 1, 1), { rows: hartmann.length, label: await blFootLabel(page) })
|
||||
await search.fill('Qwertz')
|
||||
await page.waitForFunction(() => window.__wk.api.botleadCalls.some((c) => c.q === 'Qwertz'))
|
||||
await blSettled(page)
|
||||
const emptyTitle = await textOf(page.locator('.wk-bl .empty strong'))
|
||||
const emptyHint = await textOf(page.locator('.wk-bl .empty'))
|
||||
check('botleads: Suche ohne Treffer zeigt „Keine Treffer.“ mit Hinweis, ohne Tabelle und ohne Seitenzeile', emptyTitle === 'Keine Treffer.' && emptyHint.includes('Für diesen Raum und diese Suche gibt es keine Bot-Leads.') && (await page.locator('.wk-bl-table').count()) === 0 && (await page.locator('.wk-bl .table-foot').count()) === 0, { emptyTitle, emptyHint })
|
||||
check('botleads: im Leerzustand bleiben Raumfilter und Suchfeld bedienbar', (await page.getByLabel('Raum', { exact: true }).isEnabled()) && (await search.isEnabled()) && (await search.inputValue()) === 'Qwertz')
|
||||
await shoot(page, 'botleads-leer-filter.png')
|
||||
await blSelectRoom(page, 'handwerk')
|
||||
check('botleads: Raum plus Suche ohne Treffer bleibt „Keine Treffer.“', (await textOf(page.locator('.wk-bl .empty strong'))) === 'Keine Treffer.')
|
||||
await search.fill('')
|
||||
await page.waitForFunction(() => window.__wk.api.botleadCalls.at(-1).q === '' && window.__wk.api.botleadCalls.at(-1).room === 'handwerk')
|
||||
await blSettled(page)
|
||||
check('botleads: Suche leeren bringt die 15 Handwerk-Zeilen zurück', (await blRows(page).count()) === 15)
|
||||
await blSelectRoom(page, '')
|
||||
|
||||
// 9. Fehlerzustand: deutscher Satz, Filter bleibt bedienbar, Erholung beim nächsten Versuch
|
||||
await page.evaluate(() => { window.__wk.control.fault = 'down' })
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('selbst')
|
||||
await blSettled(page)
|
||||
const err = page.locator('.wk-bl .msg.err')
|
||||
const errText = await textOf(err)
|
||||
check('botleads: Dienst nicht erreichbar zeigt den deutschen Satz „Der Hustler-Dienst ist gerade nicht erreichbar.“ (role=alert)', errText === PULT_DOWN && (await err.getAttribute('role')) === 'alert', errText)
|
||||
check('botleads: die Fehlermeldung ist ein ganzer deutscher Satz ohne Technik (kein HTTP, fetch, Error, undefined)', /^[A-ZÄÖÜ][^]*[.!]$/.test(errText) && !/http|fetch|error|undefined|unbekannter fehler|\d{3}/i.test(errText), errText)
|
||||
check('botleads: im Fehlerzustand keine Tabelle, keine Seitenzeile, Filter bedienbar', (await page.locator('.wk-bl-table').count()) === 0 && (await page.locator('.wk-bl .table-foot').count()) === 0 && (await page.getByLabel('Raum', { exact: true }).isEnabled()))
|
||||
await shoot(page, 'botleads-fehler.png')
|
||||
await page.evaluate(() => { window.__wk.control.fault = 'session' })
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('laden')
|
||||
await blSettled(page)
|
||||
check('botleads: abgelaufene Anmeldung zeigt „Anmeldung abgelaufen. Bitte neu anmelden.“', (await textOf(page.locator('.wk-bl .msg.err'))) === BL_SESSION, await textOf(page.locator('.wk-bl .msg.err')))
|
||||
await shoot(page, 'botleads-fehler-anmeldung.png')
|
||||
await page.evaluate(() => { window.__wk.control.fault = null })
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('selbst')
|
||||
await blSettled(page)
|
||||
check('botleads: nach dem nächsten Versuch ist der Fehler weg und die 15 Selbstständigen stehen da', (await page.locator('.wk-bl .msg.err').count()) === 0 && (await blRows(page).count()) === 15)
|
||||
|
||||
// 10. Deep-Link ?tab=botleads&bot=<Id>: genau ein Eintrag, „Alle anzeigen“ führt zur Liste
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.health)
|
||||
await blSettled(page)
|
||||
const single = await blRead(page)
|
||||
check('botleads: Deep-Link zeigt genau den einen Eintrag, hervorgehoben (aria-current)', single.length === 1 && single[0].title === BL_NAMES.health && single[0].current === 'true', single)
|
||||
check('botleads: Deep-Link öffnet den Reiter „Bot-Leads“', (await page.getByRole('tab', { name: 'Bot-Leads', exact: true }).getAttribute('aria-selected')) === 'true')
|
||||
check('botleads: Einzelansicht: Hinweis und „Alle anzeigen“, kein Raumfilter, keine Suche, keine Seitenzeile', (await page.getByRole('button', { name: 'Alle anzeigen', exact: true }).count()) === 1 && (await textOf(page.locator('.wk-bl-single'))).includes('Einzelner Eintrag aus dem Ereignisstrom') && (await page.locator('.wk-bl select, .wk-bl input').count()) === 0 && (await page.locator('.wk-bl .table-foot').count()) === 0)
|
||||
check('botleads: Einzelansicht des Heilberufs ohne Telefon', single[0].phone === BL_HEALTH_PHONE, single[0].phone)
|
||||
const callSingle = (await blCalls(page)).at(-1)
|
||||
check('botleads: Deep-Link fragt den Dienst mit id=<Kennung>', callSingle.id === BL_IDS.health, callSingle)
|
||||
await shoot(page, 'botleads-einzeln.png')
|
||||
await page.getByRole('button', { name: 'Alle anzeigen', exact: true }).click()
|
||||
await blSettled(page)
|
||||
check('botleads: „Alle anzeigen“ führt zur Liste (50 Zeilen, Raumfilter und Seitenzeile, kein Einzelhinweis)', (await blRows(page).count()) === 50 && (await page.locator('.wk-bl-single').count()) === 0 && (await page.getByLabel('Raum', { exact: true }).count()) === 1 && (await blFootLabel(page)) === BL_LABEL(1, 2, 60) && (await page.locator('tr[aria-current]').count()) === 0)
|
||||
metrics.botleadsFocusAfterAlleAnzeigen = await blFocus(page)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'botleads-nach-alle-anzeigen.png')
|
||||
|
||||
// gekürzter Eintrag: nur über den Deep-Link, grau, ohne Quelle
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.cut)
|
||||
await blSettled(page)
|
||||
const cut = await blRead(page)
|
||||
check('botleads: gekürzter Eintrag erscheint über den Deep-Link, mit Kürzel „gekürzt“, grauer Zeile, ohne Quelle', cut.length === 1 && cut[0].title === '[gekürzt]' && cut[0].tag === 'gekürzt' && cut[0].cut && cut[0].source === '–' && cut[0].link === null && cut[0].phone === BL_NO_PHONE, cut)
|
||||
await shoot(page, 'botleads-einzeln-gekuerzt.png')
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.cutHealth)
|
||||
await blSettled(page)
|
||||
const cutHealth = await blRead(page)
|
||||
check('botleads: gekürzter Heilberuf zeigt ebenfalls keine Nummer', cutHealth.length === 1 && cutHealth[0].phone === BL_HEALTH_PHONE, cutHealth)
|
||||
await page.goto(base + BL_URL + '&bot=gibt-es-nicht')
|
||||
await blSettled(page)
|
||||
check('botleads: unbekannte Kennung zeigt „Dieser Eintrag ist nicht (mehr) in der Liste.“ und „Alle anzeigen“', (await textOf(page.locator('.wk-bl .empty strong'))) === 'Dieser Eintrag ist nicht (mehr) in der Liste.' && (await page.getByRole('button', { name: 'Alle anzeigen', exact: true }).count()) === 1)
|
||||
await shoot(page, 'botleads-einzeln-unbekannt.png')
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.health)
|
||||
await blSettled(page)
|
||||
await page.getByRole('tab', { name: 'Freigabe', exact: true }).click()
|
||||
await page.getByRole('tab', { name: 'Bot-Leads', exact: true }).click()
|
||||
await blSettled(page)
|
||||
check('botleads: Reiterwechsel lässt die Einzelansicht fallen (wieder die ganze Liste)', (await blRows(page).count()) === 50)
|
||||
|
||||
// 11. Leerzustand ohne Filter
|
||||
await page.goto(base + BL_URL + '&botleads=leer')
|
||||
await blSettled(page)
|
||||
check('botleads: leere Liste ohne Filter zeigt „Noch keine Bot-Leads.“ ohne Tabelle und ohne Seitenzeile', (await textOf(page.locator('.wk-bl .empty strong'))) === 'Noch keine Bot-Leads.' && (await page.locator('.wk-bl-table').count()) === 0 && (await page.locator('.wk-bl .table-foot').count()) === 0)
|
||||
await shoot(page, 'botleads-leer.png')
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario R: Bot-Leads am Handy (390 x 844) ----------
|
||||
|
||||
async function handyBotleads(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'handy-botleads', { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true })
|
||||
const width = () => page.evaluate(() => ({ scroll: document.scrollingElement.scrollWidth, inner: window.innerWidth }))
|
||||
const heightOf = (name) => page.getByRole('button', { name, exact: true }).evaluate((el) => {
|
||||
el.scrollIntoView({ block: 'center' })
|
||||
const r = el.getBoundingClientRect()
|
||||
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
|
||||
return { height: Math.round(r.height * 10) / 10, width: Math.round(r.width), left: Math.round(r.left), right: Math.round(r.right), covered: !(hit && (hit === el || el.contains(hit))) }
|
||||
})
|
||||
|
||||
// 1. Reiter am Handy: im Fenster, wählbar
|
||||
await page.goto(base + '/index.html?page=1&fault=none')
|
||||
await page.getByRole('tab').first().waitFor()
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-wk-worker]').length === 19)
|
||||
const tab = page.getByRole('tab', { name: 'Bot-Leads', exact: true })
|
||||
const tabBox = await tab.evaluate((el) => { const r = el.getBoundingClientRect(); return { left: Math.round(r.left), right: Math.round(r.right), inner: window.innerWidth } })
|
||||
check('handy-botleads: Reiter „Bot-Leads“ liegt im Fenster (links >= 0, rechts <= 390) und ist nicht gewählt', tabBox.left >= 0 && tabBox.right <= tabBox.inner && (await tab.getAttribute('aria-selected')) === 'false', tabBox)
|
||||
await tab.click()
|
||||
await blSettled(page)
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
check('handy-botleads: nach dem Antippen ist „Bot-Leads“ gewählt und die Liste da', (await tab.getAttribute('aria-selected')) === 'true' && (await blRows(page).count()) === 50)
|
||||
|
||||
// 2. Kein seitliches Scrollen der Seite; die Tabelle scrollt in ihrem Rahmen
|
||||
const w0 = await width()
|
||||
metrics.phoneBotleads = w0
|
||||
check('handy-botleads: Liste ohne seitliches Scrollen (scrollingElement.scrollWidth <= innerWidth)', w0.scroll <= w0.inner, w0)
|
||||
const frame = await page.locator('.wk-bl-scroll').evaluate((el) => { const r = el.getBoundingClientRect(); return { left: Math.round(r.left), right: Math.round(r.right), clientWidth: el.clientWidth, scrollWidth: el.scrollWidth } })
|
||||
check('handy-botleads: der Rahmen der Tabelle liegt im Fenster und scrollt waagerecht (Tabelle breiter als der Rahmen)', frame.left >= 0 && frame.right <= 390 && frame.scrollWidth > frame.clientWidth, frame)
|
||||
const bar = await page.evaluate(() => {
|
||||
const out = {}
|
||||
for (const [name, sel] of [['tabs', '.tabs'], ['raum', '.wk-bl select'], ['suche', '.wk-bl input[type="search"]']]) { const r = document.querySelector(sel).getBoundingClientRect(); out[name] = { left: Math.round(r.left), right: Math.round(r.right) } }
|
||||
return out
|
||||
})
|
||||
check('handy-botleads: Reiterleiste, Raumfilter und Suchfeld liegen im Fenster', Object.values(bar).every((b) => b.left >= 0 && b.right <= 390), bar)
|
||||
const longCells = await page.evaluate(() => Array.from(document.querySelectorAll('.wk-bl-table tbody tr')).filter((tr) => tr.children[0].textContent.startsWith('Elektroinstallation-und-Haustechnik')).map((tr) => Array.from(tr.children).map((td) => td.scrollWidth - td.clientWidth)))
|
||||
check('handy-botleads: der lange Name und der lange Grund laufen nicht aus ihren Zellen', longCells.length === 1 && longCells[0].every((d) => d <= 1), longCells)
|
||||
const broken1 = await blBrokenWords(page, '.wk-bl-table tbody')
|
||||
check('handy-botleads: Liste ohne mitten im Wort umgebrochene Wörter (Name, Ort, Raum, Grund)', broken1.error === null && broken1.count > 100 && broken1.words.length === 0, { count: broken1.count, words: broken1.words.slice(0, 8) })
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-botleads-liste.png')
|
||||
await shoot(page, 'handy-botleads-liste-ganz.png', { fullPage: true })
|
||||
await page.locator('.wk-bl-scroll').evaluate((el) => { el.scrollLeft = el.scrollWidth })
|
||||
await page.locator('.wk-bl-scroll').scrollIntoViewIfNeeded()
|
||||
await sleep(150)
|
||||
await shoot(page, 'handy-botleads-tabelle-gescrollt.png')
|
||||
await page.locator('.wk-bl-scroll').evaluate((el) => { el.scrollLeft = 0 })
|
||||
|
||||
// 3. Blätterknöpfe: im Fenster, nicht verdeckt, mindestens 40 px hoch
|
||||
const prevBox = await heightOf('Zurück')
|
||||
const nextBox = await heightOf('Weiter')
|
||||
metrics.phoneBotleadsPager = { prev: prevBox, next: nextBox }
|
||||
check('handy-botleads: Blätterknöpfe „Zurück“ und „Weiter“ sind im Fenster und nicht verdeckt', [prevBox, nextBox].every((b) => b.left >= 0 && b.right <= 390 && !b.covered), { prevBox, nextBox })
|
||||
check('handy-botleads: Blätterknöpfe sind mindestens 40 px hoch (Tippfläche)', prevBox.height >= 40 && nextBox.height >= 40, { prev: prevBox.height, next: nextBox.height })
|
||||
await shoot(page, 'handy-botleads-blaettern.png')
|
||||
await page.getByRole('button', { name: 'Weiter', exact: true }).click()
|
||||
await blWaitLabel(page, BL_LABEL(2, 2, 60))
|
||||
await blSettled(page)
|
||||
const w1 = await width()
|
||||
check('handy-botleads: Seite 2 (10 Zeilen) ohne seitliches Scrollen, „Zurück“ erreichbar', (await blRows(page).count()) === 10 && w1.scroll <= w1.inner && !(await heightOf('Zurück')).covered, w1)
|
||||
await shoot(page, 'handy-botleads-seite-2.png')
|
||||
|
||||
// 4. Gekürzter Eintrag: Kontrast der abgeblendeten Zeile
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.cut)
|
||||
await blSettled(page)
|
||||
const cutRows = await blRead(page)
|
||||
check('handy-botleads: gekürzter Eintrag ist als graue Zeile da (Kürzel „gekürzt“)', cutRows.length === 1 && cutRows[0].cut && cutRows[0].tag === 'gekürzt', cutRows)
|
||||
const w2 = await width()
|
||||
check('handy-botleads: Einzelansicht ohne seitliches Scrollen', w2.scroll <= w2.inner, w2)
|
||||
const broken2 = await blBrokenWords(page, '.wk-bl-table tbody')
|
||||
check('handy-botleads: Einzelansicht ohne mitten im Wort umgebrochene Wörter (z. B. „Kaiserslauter-n“ im Betrieb-Feld)', broken2.error === null && broken2.count >= 5 && broken2.words.length === 0, { count: broken2.count, words: broken2.words })
|
||||
const cutContrast = await blContrast(page, '.wk-bl-table tbody tr.wk-bl-cut')
|
||||
metrics.phoneBotleadsContrastCut = cutContrast
|
||||
check('handy-botleads: Kontrast der abgeblendeten (gekürzten) Zeile >= 4,5:1 für jeden Text (aus den berechneten Stilen)', cutContrast.error === null && cutContrast.count >= 6 && cutContrast.low.length === 0, cutContrast)
|
||||
const alle = await heightOf('Alle anzeigen')
|
||||
check('handy-botleads: „Alle anzeigen“ ist im Fenster und mindestens 40 px hoch (Tippfläche)', alle.left >= 0 && alle.right <= 390 && !alle.covered && alle.height >= 40, alle)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-botleads-gekuerzt.png')
|
||||
|
||||
// 5. Kontrast der normalen Zeilen (Seite 1), Leerzustand und Fehler am Handy
|
||||
await page.goto(base + BL_URL)
|
||||
await blSettled(page)
|
||||
const normalContrast = await blContrast(page, '.wk-bl-table tbody')
|
||||
metrics.phoneBotleadsContrastRows = { count: normalContrast.count, min: normalContrast.min }
|
||||
check('handy-botleads: Kontrast der normalen Zeilen >= 4,5:1 für jeden Text', normalContrast.error === null && normalContrast.count > 100 && normalContrast.low.length === 0, { count: normalContrast.count, min: normalContrast.min, low: normalContrast.low.slice(0, 3) })
|
||||
const search = page.getByLabel('Betriebe suchen', { exact: true })
|
||||
await search.fill('Qwertz')
|
||||
await page.waitForFunction(() => window.__wk.api.botleadCalls.some((c) => c.q === 'Qwertz'))
|
||||
await blSettled(page)
|
||||
const w3 = await width()
|
||||
check('handy-botleads: Leerzustand mit Filter („Keine Treffer.“) ohne seitliches Scrollen', (await textOf(page.locator('.wk-bl .empty strong'))) === 'Keine Treffer.' && w3.scroll <= w3.inner, w3)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-botleads-leer-filter.png')
|
||||
await search.fill('')
|
||||
await page.waitForFunction(() => window.__wk.api.botleadCalls.at(-1).q === '')
|
||||
await blSettled(page)
|
||||
await page.evaluate(() => { window.__wk.control.fault = 'down' })
|
||||
await page.getByLabel('Raum', { exact: true }).selectOption('laden')
|
||||
await blSettled(page)
|
||||
const w4 = await width()
|
||||
check('handy-botleads: Fehlerzustand zeigt den deutschen Satz und läuft nicht über den Rand', (await textOf(page.locator('.wk-bl .msg.err'))) === PULT_DOWN && w4.scroll <= w4.inner, w4)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-botleads-fehler.png')
|
||||
await page.evaluate(() => { window.__wk.control.fault = null })
|
||||
|
||||
// 6. Deep-Link und leere Liste am Handy
|
||||
await page.goto(base + BL_URL + '&bot=' + BL_IDS.health)
|
||||
await blSettled(page)
|
||||
const one = await blRead(page)
|
||||
const w5 = await width()
|
||||
check('handy-botleads: Deep-Link zeigt genau einen Eintrag (hervorgehoben) mit „Alle anzeigen“, ohne seitliches Scrollen', one.length === 1 && one[0].current === 'true' && one[0].phone === BL_HEALTH_PHONE && (await page.getByRole('button', { name: 'Alle anzeigen', exact: true }).count()) === 1 && w5.scroll <= w5.inner, { rows: one.length, w5 })
|
||||
const broken3 = await blBrokenWords(page, '.wk-bl-table tbody')
|
||||
check('handy-botleads: Deep-Link-Einzelansicht des Heilberufs ohne mitten im Wort umgebrochene Wörter (z. B. „Kaiserslau-tern“)', broken3.error === null && broken3.count >= 5 && broken3.words.length === 0, { count: broken3.count, words: broken3.words })
|
||||
await shoot(page, 'handy-botleads-einzeln.png')
|
||||
await page.getByRole('button', { name: 'Alle anzeigen', exact: true }).click()
|
||||
await blSettled(page)
|
||||
check('handy-botleads: „Alle anzeigen“ führt am Handy zur Liste (50 Zeilen, Raumfilter da)', (await blRows(page).count()) === 50 && (await page.getByLabel('Raum', { exact: true }).count()) === 1)
|
||||
await page.goto(base + BL_URL + '&botleads=leer')
|
||||
await blSettled(page)
|
||||
const w6 = await width()
|
||||
check('handy-botleads: leere Liste („Noch keine Bot-Leads.“) ohne seitliches Scrollen', (await textOf(page.locator('.wk-bl .empty strong'))) === 'Noch keine Bot-Leads.' && w6.scroll <= w6.inner, w6)
|
||||
await shoot(page, 'handy-botleads-leer.png')
|
||||
await context.close()
|
||||
}
|
||||
|
||||
const SCENARIOS = [
|
||||
['desktop', desktop], ['blasen', bubbles], ['handy', phone], ['reduziert', reduced], ['notbremse', halted], ['ausfall', outage],
|
||||
['freigabe', freigabe], ['interessent', interessent], ['anhalten', anhalten], ['handy-freigabe', handyFreigabe],
|
||||
['antworten', antworten], ['vorschau', vorschau], ['einfuegen', einfuegen], ['bauen', bauen], ['fokus', fokus], ['handy-p4', handyP4],
|
||||
['botleads', botleads], ['handy-botleads', handyBotleads],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
|
||||
Reference in new issue
Block a user