|
|
|
@@ -4,6 +4,10 @@
|
|
|
|
|
// scripts/workers-harness/shot.cjs <distDir> <outDir>
|
|
|
|
|
// <distDir> = Ausgabe von build.mjs, <outDir> = NEUER oder leerer Ordner unter dem Temp-Ordner (TMPDIR, sonst /tmp) oder unter .superpowers/ im Repo.
|
|
|
|
|
// Ein kleiner Server auf 127.0.0.1 liefert distDir und /fonts/* aus public/fonts. Am Ende steht eine JSON-Zeile und `OK`; bei einem Verstoß Exit 1.
|
|
|
|
|
// 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). 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'
|
|
|
|
|
const { createRequire } = require('module')
|
|
|
|
|
const path = require('path')
|
|
|
|
@@ -195,6 +199,8 @@ async function desktop(browser, base) {
|
|
|
|
|
// 5. Pausenknopf und Einstellungen
|
|
|
|
|
const pauseBtn = page.getByRole('button', { name: 'Alles anhalten', exact: true })
|
|
|
|
|
await pauseBtn.click()
|
|
|
|
|
// Anhalten betrifft alle Mitarbeiter und fragt nach (P2-T21): erst „Anhalten“ im Dialog „Alles anhalten?“ pausiert
|
|
|
|
|
await page.getByRole('dialog', { name: 'Alles anhalten?', exact: true }).getByRole('button', { name: 'Anhalten', exact: true }).click()
|
|
|
|
|
await page.getByRole('button', { name: 'Fortsetzen', exact: true }).waitFor()
|
|
|
|
|
check('Pausenknopf: „Fortsetzen“ nach dem Anhalten', true)
|
|
|
|
|
await page.getByRole('button', { name: 'Fortsetzen', exact: true }).click()
|
|
|
|
@@ -471,19 +477,511 @@ async function outage(browser, base) {
|
|
|
|
|
await hang.context.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- P3: Freigabe-Pult, Interessenten-Panel, Anhalten mit Rückfrage ----------
|
|
|
|
|
// Die Seite läuft als echte WorkersPage (?page=1) über den echten API-Client und die Fetch-Weiche (&fault=none); Anmeldung, Mitarbeiterliste
|
|
|
|
|
// und Sitzungs-JWT sind ersetzt (build.mjs). Zwischenablage und window.open sind ersetzt und halten fest, was die Oberfläche tut
|
|
|
|
|
// (window.__clip, window.__opened). Die Schalter der Skript-API (window.__wk.api.faults, .pultEmpty, .pultCalls) steuern 409, 400, Ausfall und Leerzustand.
|
|
|
|
|
|
|
|
|
|
const T_MAIL = 'Anfrage: Vereinswebsite für den SV Namborn'
|
|
|
|
|
const T_PLAT = 'Kleinanzeigen: Website für Friseursalon gesucht'
|
|
|
|
|
const T_KOLL = 'Kleinanzeigen: Homepage für Handwerksbetrieb'
|
|
|
|
|
const T_UNS = 'Unsichere Antwort: bitte prüfen'
|
|
|
|
|
const T_ALT = 'freelancermap: WordPress-Relaunch'
|
|
|
|
|
const T_GES = 'Kleinanzeigen: Fotograf sucht Portfolio-Seite'
|
|
|
|
|
const T_ERL = 'Beschwerde über eine Vorschau'
|
|
|
|
|
const HAIR_URL = 'https://www.kleinanzeigen.de/s-anzeige/website-fuer-friseursalon/2870000001-1-1'
|
|
|
|
|
const WEBER_URL = 'https://www.kleinanzeigen.de/s-anzeige/homepage-handwerksbetrieb/2870000002-1-1'
|
|
|
|
|
const SV_URL = 'https://www.freelancermap.de/projekt/website-sportverein'
|
|
|
|
|
const PULT_URL = '/index.html?page=1&fault=none'
|
|
|
|
|
const PULT_DOWN = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
|
|
|
|
|
const CONFLICT = 'Wird schon von einem Kollegen bearbeitet.'
|
|
|
|
|
const TOO_LONG = 'Der Text ist zu lang (höchstens 8000 Zeichen).'
|
|
|
|
|
const HALT_QUESTION = 'Die Worker hören sofort auf, bis jemand fortsetzt.'
|
|
|
|
|
// Telefon: das Wort oder eine Nummer (+49 / Vorwahl mit 0, Trennzeichen, weitere Ziffern); Postleitzahlen und Beträge passen nicht darauf
|
|
|
|
|
const PHONE_WORD = /telefon|phone|\btel\.|handynummer|mobilnummer/i
|
|
|
|
|
const PHONE_NUMBER = /\+49|\b0\d{2,5}[\s/-]\d{3,}/
|
|
|
|
|
|
|
|
|
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
|
|
|
const rowOf = (page, title) => page.locator('li.wk-pult-row').filter({ has: page.locator('.wk-pult-title', { hasText: title }) })
|
|
|
|
|
const rowTitles = (page) => page.locator('.wk-pult-title').allTextContents()
|
|
|
|
|
const finishedLine = (page) => page.locator('.wk-pult-filters ~ .msg')
|
|
|
|
|
const setPultFault = (page, key, value) => page.evaluate(([k, v]) => { window.__wk.api.faults[k] = v }, [key, value])
|
|
|
|
|
const pultCalls = (page) => page.evaluate(() => window.__wk.api.pultCalls)
|
|
|
|
|
const resetStubs = (page) => page.evaluate(() => { window.__clip.length = 0; window.__opened.length = 0; window.__openMeta.length = 0 })
|
|
|
|
|
const stubState = (page) => page.evaluate(() => ({ clip: window.__clip.slice(), opened: window.__opened.map((a) => Array.from(a)), meta: window.__openMeta.slice() }))
|
|
|
|
|
const waitTitles = (page, titles, timeout = 5000) => page.waitForFunction(
|
|
|
|
|
(want) => JSON.stringify(Array.from(document.querySelectorAll('.wk-pult-title')).map((el) => el.textContent)) === JSON.stringify(want),
|
|
|
|
|
titles,
|
|
|
|
|
{ timeout },
|
|
|
|
|
)
|
|
|
|
|
const activeProbe = (page) => page.evaluate(() => (document.activeElement ? document.activeElement.getAttribute('data-probe') : null))
|
|
|
|
|
const activeTag = (page) => page.evaluate(() => (document.activeElement ? document.activeElement.tagName : null))
|
|
|
|
|
|
|
|
|
|
// Seite mit ersetzter Zwischenablage und ersetztem window.open (beides vor dem Laden der Seite)
|
|
|
|
|
async function newPultPage(browser, label, contextOptions) {
|
|
|
|
|
const made = await newPage(browser, label, contextOptions)
|
|
|
|
|
// Die Zwischenablage antwortet erst nach 300 ms (wie ein Browser, der nach der Erlaubnis fragt): wer vor dem Öffnen auf das Kopieren
|
|
|
|
|
// wartet, öffnet später als 300 ms nach dem Klick und verliert die Nutzer-Geste. window.open hält Zeit und Geste fest.
|
|
|
|
|
await made.context.addInitScript(() => {
|
|
|
|
|
window.__clip = []
|
|
|
|
|
window.__opened = []
|
|
|
|
|
window.__openMeta = []
|
|
|
|
|
window.__lastClickAt = 0
|
|
|
|
|
document.addEventListener('click', () => { window.__lastClickAt = performance.now() }, true)
|
|
|
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: { writeText: (text) => { window.__clip.push(text); return new Promise((resolve) => setTimeout(resolve, 300)) } },
|
|
|
|
|
})
|
|
|
|
|
window.open = (...args) => {
|
|
|
|
|
window.__opened.push(args)
|
|
|
|
|
window.__openMeta.push({ sinceClick: performance.now() - window.__lastClickAt, active: navigator.userActivation ? navigator.userActivation.isActive : null })
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return made
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reiter „Freigabe“ per Deep-Link öffnen und warten, bis die vier wartenden Einträge und die Mitarbeiternamen da sind
|
|
|
|
|
async function openPult(page, base) {
|
|
|
|
|
await page.goto(base + PULT_URL + '&tab=freigabe')
|
|
|
|
|
await page.waitForFunction(() => document.querySelectorAll('li.wk-pult-row').length === 4)
|
|
|
|
|
await page.waitForFunction(() => document.body.textContent.includes('übernommen von Justin'))
|
|
|
|
|
await page.evaluate(() => document.fonts.ready)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Szenario G: Freigabe (Desktop 1280 x 900) ----------
|
|
|
|
|
|
|
|
|
|
async function freigabe(browser, base) {
|
|
|
|
|
const { context, page } = await newPultPage(browser, 'freigabe', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
|
|
|
|
|
|
|
|
|
// 1. Reiterleiste: erst ab zwei Reitern, Start auf „Hochhaus“, Wechsel auf „Freigabe“
|
|
|
|
|
await page.goto(base + PULT_URL)
|
|
|
|
|
await page.getByRole('tab').first().waitFor()
|
|
|
|
|
await page.waitForFunction(() => document.querySelectorAll('[data-wk-worker]').length === 19)
|
|
|
|
|
const tabNames = (await page.getByRole('tab').allTextContents()).map((t) => t.trim())
|
|
|
|
|
check('freigabe: Reiterleiste mit „Hochhaus“ und „Freigabe“', same(tabNames, ['Hochhaus', 'Freigabe']), tabNames)
|
|
|
|
|
check('freigabe: Start auf „Hochhaus“ (Hochhaus mit 19 Figuren, Reiter gewählt)', (await page.getByRole('tab', { name: 'Hochhaus', exact: true }).getAttribute('aria-selected')) === 'true')
|
|
|
|
|
await page.getByRole('tab', { name: 'Freigabe', exact: true }).click()
|
|
|
|
|
await page.waitForFunction(() => document.querySelectorAll('li.wk-pult-row').length === 4)
|
|
|
|
|
await page.waitForFunction(() => document.body.textContent.includes('übernommen von Justin'))
|
|
|
|
|
await page.evaluate(() => document.fonts.ready)
|
|
|
|
|
check('freigabe: nach dem Klick ist „Freigabe“ gewählt und das Hochhaus weg', (await page.getByRole('tab', { name: 'Freigabe', exact: true }).getAttribute('aria-selected')) === 'true' && (await page.locator('[data-wk-worker]').count()) === 0)
|
|
|
|
|
|
|
|
|
|
// 2. Filter und Liste
|
|
|
|
|
const filterButtons = page.locator('.wk-pult-filters .seg button')
|
|
|
|
|
check('freigabe: Filter „Wartet“, „Veraltet“, „Erledigt“, „Wartet“ gedrückt', same(await filterButtons.allTextContents(), ['Wartet', 'Veraltet', 'Erledigt']) && same(await filterButtons.evaluateAll((els) => els.map((el) => el.getAttribute('aria-pressed'))), ['true', 'false', 'false']))
|
|
|
|
|
const first = await rowTitles(page)
|
|
|
|
|
check('freigabe: „Wartet“ zeigt die vier Einträge, neueste zuerst', same(first, [T_MAIL, T_PLAT, T_KOLL, T_UNS]), first)
|
|
|
|
|
|
|
|
|
|
const mail = rowOf(page, T_MAIL)
|
|
|
|
|
const plat = rowOf(page, T_PLAT)
|
|
|
|
|
const koll = rowOf(page, T_KOLL)
|
|
|
|
|
const uns = rowOf(page, T_UNS)
|
|
|
|
|
const buttonNames = (row) => row.locator('.wk-pult-actions').locator('button, a').allTextContents().then((list) => list.map((t) => t.replace(/\s+/g, ' ').trim()))
|
|
|
|
|
|
|
|
|
|
const platMeta = await textOf(plat.locator('.wk-pult-meta'))
|
|
|
|
|
check('freigabe: Plattform-Zeile: Alter, „läuft in 20 Std. ab“, Ort und Score', platMeta.includes('vor 5 Std.') && platMeta.includes('läuft in 20 Std. ab') && platMeta.includes('67655 Kaiserslautern · Score 72'), platMeta)
|
|
|
|
|
check('freigabe: Plattform-Zeile: Art-Badge „Antwort auf Plattform-Anfrage“', (await textOf(plat.locator('.wk-pult-head'))).includes('Antwort auf Plattform-Anfrage'))
|
|
|
|
|
const link = plat.getByRole('link', { name: 'Anzeige öffnen', exact: true })
|
|
|
|
|
check('freigabe: „Anzeige öffnen“ ist ein sicherer Link (https, neuer Tab, noopener noreferrer)', (await link.getAttribute('href')) === HAIR_URL && (await link.getAttribute('target')) === '_blank' && (await link.getAttribute('rel')) === 'noopener noreferrer')
|
|
|
|
|
check('freigabe: Plattform-Zeile (offen): Kopieren & öffnen, Ich kümmere mich, Als gesendet markieren, Verwerfen', same(await buttonNames(plat), ['Kopieren & öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Verwerfen']), await buttonNames(plat))
|
|
|
|
|
const mailTo = await mail.getByRole('link', { name: 'E-Mail schreiben', exact: true }).getAttribute('href')
|
|
|
|
|
check('freigabe: Mail-Zeile: Link „E-Mail schreiben“ mit mailto-Adresse, Betreff und Text', typeof mailTo === 'string' && mailTo.startsWith('mailto:vorstand@sv-namborn.example?subject=Re%3A%20SV%20Namborn&body=Guten%20Tag%20Herr%20Beispiel'), mailTo)
|
|
|
|
|
check('freigabe: Mail-Zeile: kein „Kopieren & öffnen“ (Antwort geht per Mailprogramm)', (await mail.getByRole('button', { name: 'Kopieren & öffnen' }).count()) === 0)
|
|
|
|
|
await mail.getByLabel('Entwurf der Antwort').fill('Neuer Text für die Mail.')
|
|
|
|
|
const mailEdited = await mail.getByRole('link', { name: 'E-Mail schreiben', exact: true }).getAttribute('href')
|
|
|
|
|
check('freigabe: Mail-Zeile: der Mail-Link trägt den bearbeiteten Text aus dem Feld', mailEdited === 'mailto:vorstand@sv-namborn.example?subject=Re%3A%20SV%20Namborn&body=' + encodeURIComponent('Neuer Text für die Mail.'), mailEdited)
|
|
|
|
|
check('freigabe: Kollegen-Zeile: „übernommen von Justin“, kein „Ich kümmere mich“', (await textOf(koll.locator('.wk-pult-meta'))).includes('übernommen von Justin') && (await koll.getByRole('button', { name: 'Ich kümmere mich' }).count()) === 0)
|
|
|
|
|
check('freigabe: Zeile ohne Antwort („unsicher“): nur Ich kümmere mich, Erledigt, Verwerfen, kein Textfeld', same(await buttonNames(uns), ['Ich kümmere mich', 'Erledigt', 'Verwerfen']) && (await uns.locator('textarea').count()) === 0, await buttonNames(uns))
|
|
|
|
|
await topOfPage(page)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-wartet.png')
|
|
|
|
|
await shoot(page, 'desktop-freigabe-wartet-ganz.png', { fullPage: true })
|
|
|
|
|
|
|
|
|
|
// 3. Kollege hat schon übernommen: Kopieren & öffnen kopiert und öffnet, der Dienst antwortet 409, die Zeile zeigt den Namen
|
|
|
|
|
await koll.getByRole('button', { name: 'Kopieren & öffnen', exact: true }).click()
|
|
|
|
|
const kollNote = koll.locator('.msg')
|
|
|
|
|
await kollNote.waitFor()
|
|
|
|
|
const kollText = await textOf(kollNote)
|
|
|
|
|
check('freigabe: 409 beim Übernehmen: Hinweis „Wird schon von Justin bearbeitet.“ (zusammen mit „Kopiert und geöffnet.“)', kollText === 'Kopiert und geöffnet. Wird schon von Justin bearbeitet.', kollText)
|
|
|
|
|
check('freigabe: 409-Hinweis ist eine rote Zeile mit role=alert', (await kollNote.getAttribute('class')).includes('err') && (await kollNote.getAttribute('role')) === 'alert')
|
|
|
|
|
const kollStub = await stubState(page)
|
|
|
|
|
check('freigabe: Kollegen-Zeile: Text kopiert und Anzeige geöffnet (trotz 409)', kollStub.clip.length === 1 && kollStub.clip[0].startsWith('Guten Tag, gern bauen wir Ihnen eine erste Vorschau Ihrer Homepage') && same(kollStub.opened, [[WEBER_URL, '_blank', 'noopener,noreferrer']]), kollStub)
|
|
|
|
|
check('freigabe: Kollegen-Zeile bleibt „übernommen von Justin“', (await textOf(koll.locator('.wk-pult-meta'))).includes('übernommen von Justin'))
|
|
|
|
|
await shoot(page, 'desktop-freigabe-409-kollege.png')
|
|
|
|
|
await resetStubs(page)
|
|
|
|
|
|
|
|
|
|
// 4. Kopieren & öffnen mit bearbeitetem Text: kopiert den Text aus dem Feld, öffnet die Anzeige, übernimmt den Eintrag für mich
|
|
|
|
|
const area = plat.getByLabel('Entwurf der Antwort')
|
|
|
|
|
const draft = await area.inputValue()
|
|
|
|
|
const edited = draft + '\nPS: Bearbeitet im Prüfstand.'
|
|
|
|
|
await area.fill(edited)
|
|
|
|
|
await plat.getByRole('button', { name: 'Kopieren & öffnen', exact: true }).click()
|
|
|
|
|
const platNote = plat.locator('.msg')
|
|
|
|
|
await platNote.waitFor()
|
|
|
|
|
check('freigabe: Kopieren & öffnen: Meldung „Kopiert und geöffnet.“ (grün, role=status)', (await textOf(platNote)) === 'Kopiert und geöffnet.' && (await platNote.getAttribute('class')).includes('ok') && (await platNote.getAttribute('role')) === 'status', await textOf(platNote))
|
|
|
|
|
const platStub = await stubState(page)
|
|
|
|
|
check('freigabe: Kopieren & öffnen kopiert den bearbeiteten Text aus dem Feld', same(platStub.clip, [edited]), platStub.clip)
|
|
|
|
|
check('freigabe: Kopieren & öffnen öffnet genau die Anzeige im neuen Tab (noopener,noreferrer)', same(platStub.opened, [[HAIR_URL, '_blank', 'noopener,noreferrer']]), platStub.opened)
|
|
|
|
|
check('freigabe: Kopieren & öffnen öffnet im Zug des Klicks (binnen 150 ms, Nutzer-Geste aktiv), ohne die Zwischenablage abzuwarten', platStub.meta.length === 1 && platStub.meta[0].sinceClick < 150 && platStub.meta[0].active === true, platStub.meta)
|
|
|
|
|
await page.waitForFunction((t) => {
|
|
|
|
|
const row = Array.from(document.querySelectorAll('li.wk-pult-row')).find((el) => el.textContent.includes(t))
|
|
|
|
|
return row && row.querySelector('.wk-pult-meta').textContent.includes('übernommen von Kenso')
|
|
|
|
|
}, T_PLAT)
|
|
|
|
|
const platCalls = (await pultCalls(page)).filter((c) => c.id === 'pult-plat')
|
|
|
|
|
check('freigabe: Kopieren & öffnen übernimmt den Eintrag (claim) und er steht „übernommen von Kenso“', platCalls.length >= 1 && platCalls[0].action === 'claim' && (await plat.getByRole('button', { name: 'Ich kümmere mich' }).count()) === 0, platCalls)
|
|
|
|
|
check('freigabe: der bearbeitete Text bleibt nach dem Neuladen der Liste im Feld', (await area.inputValue()) === edited)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-kopiert.png')
|
|
|
|
|
|
|
|
|
|
// 5. Übernehmen: 409 vom Dienst (Schalter) zeigt den Satz, danach klappt es
|
|
|
|
|
await setPultFault(page, 'claim', { status: 409, message: CONFLICT })
|
|
|
|
|
await mail.getByRole('button', { name: 'Ich kümmere mich', exact: true }).click()
|
|
|
|
|
const mailNote = mail.locator('.msg')
|
|
|
|
|
await mailNote.waitFor()
|
|
|
|
|
check('freigabe: Übernehmen mit 409: der Satz des Dienstes steht in der Zeile', (await textOf(mailNote)) === CONFLICT && (await mailNote.getAttribute('role')) === 'alert', await textOf(mailNote))
|
|
|
|
|
check('freigabe: nach dem 409 ist der Eintrag nicht übernommen (Knopf bleibt, kein „übernommen von“)', (await mail.getByRole('button', { name: 'Ich kümmere mich' }).count()) === 1 && !(await textOf(mail.locator('.wk-pult-meta'))).includes('übernommen von'))
|
|
|
|
|
await shoot(page, 'desktop-freigabe-409-uebernehmen.png')
|
|
|
|
|
await setPultFault(page, 'claim', null)
|
|
|
|
|
await mail.getByRole('button', { name: 'Ich kümmere mich', exact: true }).click()
|
|
|
|
|
await page.waitForFunction((t) => {
|
|
|
|
|
const row = Array.from(document.querySelectorAll('li.wk-pult-row')).find((el) => el.textContent.includes(t))
|
|
|
|
|
return row && row.querySelector('.wk-pult-meta').textContent.includes('übernommen von Kenso')
|
|
|
|
|
}, T_MAIL)
|
|
|
|
|
check('freigabe: Übernehmen: „in Arbeit“ von mir (übernommen von Kenso), Meldung „Du kümmerst dich darum.“, Knopf weg', (await textOf(mail.locator('.msg'))) === 'Du kümmerst dich darum.' && (await mail.getByRole('button', { name: 'Ich kümmere mich' }).count()) === 0, await textOf(mail.locator('.msg')))
|
|
|
|
|
await shoot(page, 'desktop-freigabe-uebernommen.png')
|
|
|
|
|
|
|
|
|
|
// 6. Als gesendet markieren: Fehler 400 mit deutschem Satz, dann der Erfolg mit dem bearbeiteten Text
|
|
|
|
|
await setPultFault(page, 'sent', { status: 400, message: TOO_LONG })
|
|
|
|
|
await plat.getByRole('button', { name: 'Als gesendet markieren', exact: true }).click()
|
|
|
|
|
await plat.locator('.msg.err').waitFor()
|
|
|
|
|
check('freigabe: Fehler 400: der deutsche Satz des Dienstes steht in der Zeile (role=alert), die Zeile bleibt', (await textOf(plat.locator('.msg.err'))) === TOO_LONG && (await plat.locator('.msg.err').getAttribute('role')) === 'alert' && (await rowTitles(page)).includes(T_PLAT), await textOf(plat.locator('.msg.err')))
|
|
|
|
|
await shoot(page, 'desktop-freigabe-400.png')
|
|
|
|
|
await setPultFault(page, 'sent', null)
|
|
|
|
|
await plat.getByRole('button', { name: 'Als gesendet markieren', exact: true }).click()
|
|
|
|
|
await finishedLine(page).first().waitFor()
|
|
|
|
|
check('freigabe: gesendet: Meldung oben „…“ als gesendet markiert. (role=status), die Zeile ist weg', (await textOf(finishedLine(page).first())) === `„${T_PLAT}“ als gesendet markiert.` && (await finishedLine(page).first().getAttribute('role')) === 'status' && !(await rowTitles(page)).includes(T_PLAT), await textOf(finishedLine(page).first()))
|
|
|
|
|
const sentCall = (await pultCalls(page)).filter((c) => c.id === 'pult-plat' && c.action === 'sent').pop()
|
|
|
|
|
check('freigabe: gesendet schickt den bearbeiteten Text (body.text)', sentCall !== undefined && sentCall.body && sentCall.body.text === edited, sentCall)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-gesendet.png')
|
|
|
|
|
|
|
|
|
|
// 7. Erledigt (Art ohne Antwort)
|
|
|
|
|
await uns.getByRole('button', { name: 'Erledigt', exact: true }).click()
|
|
|
|
|
await page.waitForFunction((t) => document.querySelector('.wk-pult-filters ~ .msg') && document.querySelector('.wk-pult-filters ~ .msg').textContent === `„${t}“ erledigt.`, T_UNS)
|
|
|
|
|
check('freigabe: Erledigt: Meldung oben, die Zeile ist weg', !(await rowTitles(page)).includes(T_UNS), await rowTitles(page))
|
|
|
|
|
|
|
|
|
|
// 8. Verwerfen: erster Klick fragt nach, die Frage verfällt nach 4 s, zwei Klicks verwerfen; die Oberfläche schickt keinen Grund
|
|
|
|
|
const discardBtn = mail.locator('.wk-pult-actions').getByRole('button', { name: /^(Verwerfen|Wirklich verwerfen\?)$/ })
|
|
|
|
|
await discardBtn.click()
|
|
|
|
|
check('freigabe: Verwerfen: erster Klick fragt „Wirklich verwerfen?“, noch keine Aktion am Dienst', (await textOf(discardBtn)) === 'Wirklich verwerfen?' && (await pultCalls(page)).filter((c) => c.action === 'discard').length === 0)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-verwerfen-frage.png')
|
|
|
|
|
await sleep(4400)
|
|
|
|
|
check('freigabe: Verwerfen: die Rückfrage verfällt nach 4 s (wieder „Verwerfen“)', (await textOf(discardBtn)) === 'Verwerfen')
|
|
|
|
|
await discardBtn.click()
|
|
|
|
|
await discardBtn.click()
|
|
|
|
|
await page.waitForFunction((t) => document.querySelector('.wk-pult-filters ~ .msg') && document.querySelector('.wk-pult-filters ~ .msg').textContent === `„${t}“ verworfen.`, T_MAIL)
|
|
|
|
|
const discards = (await pultCalls(page)).filter((c) => c.action === 'discard')
|
|
|
|
|
check('freigabe: Verwerfen: genau ein Aufruf, Meldung oben „…“ verworfen., Zeile weg', discards.length === 1 && discards[0].id === 'pult-mail' && !(await rowTitles(page)).includes(T_MAIL), discards)
|
|
|
|
|
check('freigabe: Verwerfen schickt keinen Grund (Body {}); die Oberfläche hat kein Feld dafür', same(discards[0] && discards[0].body, {}), discards[0])
|
|
|
|
|
const afterAll = await rowTitles(page)
|
|
|
|
|
check('freigabe: „Wartet“ zeigt am Ende nur den Eintrag des Kollegen', same(afterAll, [T_KOLL]), afterAll)
|
|
|
|
|
|
|
|
|
|
// 9. Filter „Veraltet“, „Erledigt“
|
|
|
|
|
await filterButtons.nth(1).click()
|
|
|
|
|
await waitTitles(page, [T_ALT])
|
|
|
|
|
check('freigabe: „Veraltet“ zeigt den veralteten Eintrag mit Badge „veraltet“', (await textOf(rowOf(page, T_ALT).locator('.wk-pult-head'))).includes('veraltet') && (await filterButtons.nth(1).getAttribute('aria-pressed')) === 'true')
|
|
|
|
|
check('freigabe: Filterwechsel räumt die Meldung oben weg', (await finishedLine(page).count()) === 0)
|
|
|
|
|
check('freigabe: veralteter Eintrag lässt sich weiter bearbeiten (Kopieren & öffnen, Ich kümmere mich, Als gesendet markieren, Verwerfen)', same(await buttonNames(rowOf(page, T_ALT)), ['Kopieren & öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Verwerfen']), await buttonNames(rowOf(page, T_ALT)))
|
|
|
|
|
await shoot(page, 'desktop-freigabe-veraltet.png')
|
|
|
|
|
await rowOf(page, T_ALT).getByRole('button', { name: 'Ich kümmere mich', exact: true }).click()
|
|
|
|
|
await page.locator('.empty').waitFor()
|
|
|
|
|
const emptyOld = await textOf(page.locator('.empty'))
|
|
|
|
|
check('freigabe: Leerzustand „Veraltet“: „Nichts ist veraltet“ mit Hinweis', emptyOld.startsWith('Nichts ist veraltet') && emptyOld.includes('Einträge, die zu lange liegen'), emptyOld)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-leer-veraltet.png')
|
|
|
|
|
|
|
|
|
|
await filterButtons.nth(2).click()
|
|
|
|
|
await waitTitles(page, [T_MAIL, T_PLAT, T_UNS, T_GES, T_ERL])
|
|
|
|
|
check('freigabe: „Erledigt“ zeigt gesendet, verworfen, erledigt (fünf Zeilen), ohne Aktionsknöpfe', (await page.locator('.wk-pult-actions').count()) === 0)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-erledigt.png')
|
|
|
|
|
|
|
|
|
|
// 10. Leerzustand „Wartet“ und Ausfall des Pults mit „Neu laden“
|
|
|
|
|
await page.evaluate(() => { window.__wk.api.pultEmpty = true })
|
|
|
|
|
await filterButtons.nth(0).click()
|
|
|
|
|
await page.locator('.empty').waitFor()
|
|
|
|
|
const emptyWait = await textOf(page.locator('.empty'))
|
|
|
|
|
check('freigabe: Leerzustand „Wartet“: „Nichts wartet auf dich“ mit Hinweis', emptyWait.startsWith('Nichts wartet auf dich') && emptyWait.includes('Neue Antworten erscheinen hier, sobald ein Entwurf bereit ist.'), emptyWait)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-leer.png')
|
|
|
|
|
await setPultFault(page, 'pult', { status: 503, message: PULT_DOWN })
|
|
|
|
|
await filterButtons.nth(1).click()
|
|
|
|
|
await page.getByRole('button', { name: 'Neu laden', exact: true }).waitFor()
|
|
|
|
|
const emptyDown = await textOf(page.locator('.empty'))
|
|
|
|
|
check('freigabe: Pult nicht erreichbar: Hinweis mit dem Satz des Dienstes und „Neu laden“', emptyDown.includes('Das Pult ist gerade nicht erreichbar') && emptyDown.includes(PULT_DOWN), emptyDown)
|
|
|
|
|
await shoot(page, 'desktop-freigabe-ausfall.png')
|
|
|
|
|
await setPultFault(page, 'pult', null)
|
|
|
|
|
await page.getByRole('button', { name: 'Neu laden', exact: true }).click()
|
|
|
|
|
await page.waitForFunction(() => (document.querySelector('.empty') || {}).textContent?.startsWith('Nichts ist veraltet'))
|
|
|
|
|
check('freigabe: „Neu laden“ nach dem Ausfall fragt erneut, der Leerzustand kehrt zurück', true)
|
|
|
|
|
await page.evaluate(() => { window.__wk.api.pultEmpty = false })
|
|
|
|
|
await filterButtons.nth(0).click()
|
|
|
|
|
await waitTitles(page, [T_KOLL, T_ALT])
|
|
|
|
|
check('freigabe: nach dem Ausfall und dem Leerzustand ist die Liste wieder da (Kollege und der eben übernommene veraltete Eintrag)', true)
|
|
|
|
|
await context.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Szenario H: Interessenten-Panel (Desktop) ----------
|
|
|
|
|
|
|
|
|
|
async function interessent(browser, base) {
|
|
|
|
|
const { context, page } = await newPultPage(browser, 'interessent', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
|
|
|
|
await openPult(page, base)
|
|
|
|
|
|
|
|
|
|
// 1. „Details“ öffnet das Panel als Dialog
|
|
|
|
|
const opener = rowOf(page, T_MAIL).getByRole('button', { name: 'Details', exact: true })
|
|
|
|
|
await opener.evaluate((el) => el.setAttribute('data-probe', 'details-opener'))
|
|
|
|
|
await opener.click()
|
|
|
|
|
const dialog = page.getByRole('dialog', { name: 'SV Namborn', exact: true })
|
|
|
|
|
await dialog.waitFor()
|
|
|
|
|
await dialog.locator('.wk-prospect').waitFor()
|
|
|
|
|
await sleep(600) // der Dialog blendet ein
|
|
|
|
|
check('interessent: „Details“ öffnet einen Dialog (role=dialog, aria-modal) mit dem Namen als Titel', (await dialog.getAttribute('aria-modal')) === 'true')
|
|
|
|
|
// Im Entwicklungsmodus hängt StrictMode jeden Effekt zweimal ein: ein oder zwei Abfragen, aber immer nur dieser Interessent
|
|
|
|
|
const asked = await page.evaluate(() => window.__wk.api.prospectCalls)
|
|
|
|
|
check('interessent: das Panel fragt den Interessenten p-mail ab (und keinen anderen)', asked.length >= 1 && asked.every((id) => id === 'p-mail'), asked)
|
|
|
|
|
check('interessent: der Fokus liegt beim Öffnen im Dialog', await dialog.evaluate((el) => el.contains(document.activeElement)))
|
|
|
|
|
const scoreParts = await dialog.locator('.wk-prospect-score').evaluate((el) => Array.from(el.children).map((child) => child.textContent.trim()))
|
|
|
|
|
check('interessent: Score als Badge „Score 84 · hoch“ mit Begründung', same(scoreParts, ['Score 84 · hoch', 'Klarer Auftrag, Budget genannt, Region passt.']), scoreParts)
|
|
|
|
|
const facts = await dialog.locator('dl.kv').evaluate((dl) => Array.from(dl.children).map((el) => el.textContent.trim()))
|
|
|
|
|
check('interessent: Fakten Quelle, Gefunden, Ort, Status (ohne Telefon)', same(facts, ['Quelle', 'freelancermap', 'Gefunden', 'vor 3 Std.', 'Ort', '66640 Namborn', 'Status', 'Bewertet']), facts)
|
|
|
|
|
const request = dialog.locator('.wk-prospect-section').first()
|
|
|
|
|
const requestText = request.locator('.wk-pult-text')
|
|
|
|
|
check('interessent: Anfragetext als Text mit erhaltenen Zeilenumbrüchen (pre-wrap)', (await requestText.textContent()).includes('Vereinsseite neu macht.\n\nBudget bis 1.500 Euro') && (await requestText.evaluate((el) => getComputedStyle(el).whiteSpace)) === 'pre-wrap')
|
|
|
|
|
const open = request.getByRole('link', { name: 'Anfrage öffnen', exact: true })
|
|
|
|
|
check('interessent: „Anfrage öffnen“ ist ein sicherer Link (https, neuer Tab, noopener noreferrer)', (await open.getAttribute('href')) === SV_URL && (await open.getAttribute('target')) === '_blank' && (await open.getAttribute('rel')) === 'noopener noreferrer')
|
|
|
|
|
const msgs = await dialog.locator('li.wk-prospect-msg').evaluateAll((els) => els.map((el) => ({ who: el.querySelector('strong').textContent, when: el.querySelector('.wk-prospect-msg-head span').textContent, body: el.querySelector('p').textContent })))
|
|
|
|
|
check('interessent: zwei Nachrichten: „Interessent“ (eingegangen) und „Justin“ (Mitarbeiter, Name aus der Liste), mit Alter', msgs.length === 2 && msgs[0].who === 'Interessent' && msgs[1].who === 'Justin' && msgs[0].when === 'vor 3 Std.' && msgs[1].when === 'vor 2 Std.' && msgs[0].body.startsWith('Guten Tag,\nkönnen Sie uns eine Vorschau zeigen?'), msgs)
|
|
|
|
|
const previewLine = await textOf(dialog.locator('.wk-prospect-section').last().locator('p.muted'))
|
|
|
|
|
check('interessent: Vorschau-Zeile „Vorschau: fertig · Runde 1 · erstellt vor 1 Std.“', previewLine === 'Vorschau: fertig · Runde 1 · erstellt vor 1 Std.', previewLine)
|
|
|
|
|
const html = await page.evaluate(() => document.documentElement.outerHTML)
|
|
|
|
|
const text = await page.evaluate(() => document.body.innerText)
|
|
|
|
|
check('interessent: nirgends im Bild ein Telefon (weder Wort noch Nummer, weder in Text noch im HTML)', !PHONE_WORD.test(html) && !PHONE_NUMBER.test(html) && !PHONE_WORD.test(text) && !PHONE_NUMBER.test(text), { word: (html.match(PHONE_WORD) || [])[0], number: (html.match(PHONE_NUMBER) || [])[0] })
|
|
|
|
|
check('interessent: keine Mail-Rohdaten im Bild (messageId, inReplyTo, references)', !/messageId|inReplyTo|referencesJson/.test(html))
|
|
|
|
|
await shoot(page, 'desktop-interessent-panel.png')
|
|
|
|
|
|
|
|
|
|
// 2. Esc schließt, der Fokus kehrt auf „Details“ zurück
|
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('interessent: Esc schließt das Panel', true)
|
|
|
|
|
check('interessent: nach Esc steht der Fokus wieder auf „Details“', (await activeProbe(page)) === 'details-opener', await activeTag(page))
|
|
|
|
|
|
|
|
|
|
// 3. „Schließen“ im Fuß und das Kreuz schließen ebenfalls und geben den Fokus zurück
|
|
|
|
|
await opener.click()
|
|
|
|
|
await page.getByRole('dialog', { name: 'SV Namborn', exact: true }).waitFor()
|
|
|
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Schließen', exact: true }).last().click()
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('interessent: „Schließen“ im Fuß schließt das Panel, Fokus zurück auf „Details“', (await activeProbe(page)) === 'details-opener', await activeTag(page))
|
|
|
|
|
await opener.click()
|
|
|
|
|
await page.getByRole('dialog', { name: 'SV Namborn', exact: true }).waitFor()
|
|
|
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Schließen', exact: true }).first().click()
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('interessent: das Kreuz oben schließt das Panel, Fokus zurück auf „Details“', (await activeProbe(page)) === 'details-opener', await activeTag(page))
|
|
|
|
|
|
|
|
|
|
// 4. Interessent ohne Score, ohne Anfragetext, ohne Verlauf
|
|
|
|
|
await rowOf(page, T_KOLL).getByRole('button', { name: 'Details', exact: true }).click()
|
|
|
|
|
const dialog2 = page.getByRole('dialog', { name: 'Meisterbetrieb Weber', exact: true })
|
|
|
|
|
await dialog2.waitFor()
|
|
|
|
|
await dialog2.locator('.wk-prospect').waitFor()
|
|
|
|
|
const body2 = await textOf(dialog2.locator('.wk-prospect'))
|
|
|
|
|
check('interessent: ohne Score „Noch nicht bewertet“, ohne Anfragetext und Verlauf die Leerzeilen, keine Vorschau-Zeile', body2.includes('Noch nicht bewertet') && body2.includes('Kein Anfragetext gespeichert.') && body2.includes('Noch keine Nachrichten.') && !body2.includes('Vorschau'), body2)
|
|
|
|
|
const facts2 = await dialog2.locator('dl.kv').evaluate((dl) => Array.from(dl.children).map((el) => el.textContent.trim()))
|
|
|
|
|
check('interessent: Fakten Kleinanzeigen / 66849 Landstuhl / Gefunden', same(facts2, ['Quelle', 'Kleinanzeigen', 'Gefunden', 'vor 8 Std.', 'Ort', '66849 Landstuhl', 'Status', 'Gefunden']), facts2)
|
|
|
|
|
await sleep(600)
|
|
|
|
|
await shoot(page, 'desktop-interessent-leer.png')
|
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
|
|
|
|
|
// 5. Dienst meldet einen Fehler: Satz im Panel, „Neu laden“ holt den Interessenten
|
|
|
|
|
await setPultFault(page, 'prospect', { status: 404, message: 'Interessent nicht gefunden.' })
|
|
|
|
|
await rowOf(page, T_PLAT).getByRole('button', { name: 'Details', exact: true }).click()
|
|
|
|
|
const dialog3 = page.getByRole('dialog', { name: 'Interessent', exact: true })
|
|
|
|
|
await dialog3.getByRole('alert').waitFor()
|
|
|
|
|
check('interessent: Fehler vom Dienst: der Satz steht im Panel (role=alert) mit „Neu laden“', (await textOf(dialog3.getByRole('alert'))) === 'Interessent nicht gefunden.' && (await dialog3.getByRole('button', { name: 'Neu laden', exact: true }).count()) === 1, await textOf(dialog3))
|
|
|
|
|
await sleep(600)
|
|
|
|
|
await shoot(page, 'desktop-interessent-fehler.png')
|
|
|
|
|
await setPultFault(page, 'prospect', null)
|
|
|
|
|
await dialog3.getByRole('button', { name: 'Neu laden', exact: true }).click()
|
|
|
|
|
const dialog4 = page.getByRole('dialog', { name: 'Friseursalon Haarmonie', exact: true })
|
|
|
|
|
await dialog4.locator('.wk-prospect').waitFor()
|
|
|
|
|
check('interessent: „Neu laden“ holt den Interessenten (Score 72 · mittel)', (await textOf(dialog4.locator('.wk-prospect-score'))).startsWith('Score 72 · mittel'), await textOf(dialog4.locator('.wk-prospect-score')))
|
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
await context.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Szenario I: Alles anhalten mit Rückfrage (Desktop) ----------
|
|
|
|
|
|
|
|
|
|
async function anhalten(browser, base) {
|
|
|
|
|
const { context, page } = await newPage(browser, 'anhalten', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
|
|
|
|
await ready(page, base + '/index.html')
|
|
|
|
|
await sleep(400)
|
|
|
|
|
await topOfPage(page)
|
|
|
|
|
const status = () => textOf(page.locator('.wk-status'))
|
|
|
|
|
const calls = () => page.evaluate(() => window.__wk.api.pauseCalls)
|
|
|
|
|
const pauseBtn = page.locator('.wk-toolbar').getByRole('button', { name: 'Alles anhalten', exact: true })
|
|
|
|
|
|
|
|
|
|
// 1. Rückfrage statt sofortigem Anhalten
|
|
|
|
|
await pauseBtn.evaluate((el) => el.setAttribute('data-probe', 'pause-button'))
|
|
|
|
|
await pauseBtn.click()
|
|
|
|
|
const dialog = page.getByRole('dialog', { name: 'Alles anhalten?', exact: true })
|
|
|
|
|
await dialog.waitFor()
|
|
|
|
|
check('anhalten: „Alles anhalten“ öffnet den Dialog „Alles anhalten?“ mit dem Satz zu den Workern', (await textOf(dialog.locator('.dialog-body'))) === HALT_QUESTION, await textOf(dialog.locator('.dialog-body')))
|
|
|
|
|
check('anhalten: Dialog mit den Knöpfen „Abbrechen“ und „Anhalten“', same(await dialog.locator('.dialog-foot button').allTextContents(), ['Abbrechen', 'Anhalten']))
|
|
|
|
|
check('anhalten: vor der Bestätigung ist nichts angehalten (kein Aufruf, Status „Läuft“)', same(await calls(), []) && (await status()) === 'Läuft', { calls: await calls(), status: await status() })
|
|
|
|
|
await sleep(600)
|
|
|
|
|
await shoot(page, 'desktop-anhalten-frage.png')
|
|
|
|
|
|
|
|
|
|
// 2. Abbrechen tut nichts, der Fokus kehrt auf den Knopf zurück
|
|
|
|
|
await dialog.getByRole('button', { name: 'Abbrechen', exact: true }).click()
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('anhalten: „Abbrechen“ schließt den Dialog, nichts wird angehalten', same(await calls(), []) && (await status()) === 'Läuft' && (await pauseBtn.textContent()) === 'Alles anhalten')
|
|
|
|
|
check('anhalten: nach „Abbrechen“ steht der Fokus wieder auf „Alles anhalten“', (await activeProbe(page)) === 'pause-button', await activeTag(page))
|
|
|
|
|
|
|
|
|
|
// 3. Esc verhält sich wie Abbrechen
|
|
|
|
|
await pauseBtn.click()
|
|
|
|
|
await page.getByRole('dialog', { name: 'Alles anhalten?', exact: true }).waitFor()
|
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('anhalten: Esc schließt den Dialog, nichts wird angehalten, Fokus zurück auf „Alles anhalten“', same(await calls(), []) && (await activeProbe(page)) === 'pause-button', { calls: await calls(), focus: await activeTag(page) })
|
|
|
|
|
|
|
|
|
|
// 4. Anhalten hält an
|
|
|
|
|
await pauseBtn.click()
|
|
|
|
|
const dialog2 = page.getByRole('dialog', { name: 'Alles anhalten?', exact: true })
|
|
|
|
|
await dialog2.waitFor()
|
|
|
|
|
await dialog2.getByRole('button', { name: 'Anhalten', exact: true }).click()
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
await page.getByRole('button', { name: 'Fortsetzen', exact: true }).waitFor()
|
|
|
|
|
await page.waitForFunction(() => document.querySelector('.wk-status') && document.querySelector('.wk-status').textContent.startsWith('Angehalten'))
|
|
|
|
|
check('anhalten: „Anhalten“ pausiert genau einmal (Aufruf pause(true)), Status „Angehalten: …“, Knopf „Fortsetzen“', same(await calls(), [true]) && (await status()) === 'Angehalten: Es wird nichts gesendet und nichts gebaut.', { calls: await calls(), status: await status() })
|
|
|
|
|
await sleep(500)
|
|
|
|
|
const focusTag = await activeTag(page)
|
|
|
|
|
const focusText = await page.evaluate(() => (document.activeElement ? (document.activeElement.textContent || '').trim().slice(0, 40) : null))
|
|
|
|
|
check('anhalten: nach „Anhalten“ steht der Fokus nicht auf <body> (Tastatur-Nutzer verlieren sonst ihre Stelle)', focusTag !== null && focusTag !== 'BODY', { tag: focusTag, text: focusText })
|
|
|
|
|
await topOfPage(page)
|
|
|
|
|
await shoot(page, 'desktop-anhalten-angehalten.png')
|
|
|
|
|
|
|
|
|
|
// 5. Fortsetzen geht ohne Rückfrage
|
|
|
|
|
await page.getByRole('button', { name: 'Fortsetzen', exact: true }).click()
|
|
|
|
|
await page.getByRole('button', { name: 'Alles anhalten', exact: true }).waitFor()
|
|
|
|
|
check('anhalten: „Fortsetzen“ geht ohne Rückfrage (kein Dialog, pause(false), Status „Läuft“)', (await page.getByRole('dialog').count()) === 0 && same(await calls(), [true, false]) && (await status()) === 'Läuft', { calls: await calls(), status: await status() })
|
|
|
|
|
await context.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Szenario J: Handy 390 x 844 für Freigabe und Panel ----------
|
|
|
|
|
|
|
|
|
|
async function handyFreigabe(browser, base) {
|
|
|
|
|
const { context, page } = await newPultPage(browser, 'handy-freigabe', { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true })
|
|
|
|
|
await openPult(page, base)
|
|
|
|
|
await sleep(300)
|
|
|
|
|
const width = () => page.evaluate(() => ({ scroll: document.scrollingElement.scrollWidth, inner: window.innerWidth }))
|
|
|
|
|
const w0 = await width()
|
|
|
|
|
metrics.phoneFreigabe = w0
|
|
|
|
|
check('handy-freigabe: Freigabe ohne seitliches Scrollen (scrollingElement.scrollWidth <= innerWidth)', w0.scroll <= w0.inner, w0)
|
|
|
|
|
const bar = await page.evaluate(() => {
|
|
|
|
|
const out = {}
|
|
|
|
|
for (const [name, sel] of [['tabs', '.tabs'], ['filter', '.wk-pult-filters .seg']]) {
|
|
|
|
|
const r = document.querySelector(sel).getBoundingClientRect()
|
|
|
|
|
out[name] = { left: Math.round(r.left), right: Math.round(r.right) }
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
})
|
|
|
|
|
check('handy-freigabe: Reiterleiste und Filter liegen im Fenster', bar.tabs.left >= 0 && bar.tabs.right <= 390 && bar.filter.left >= 0 && bar.filter.right <= 390, bar)
|
|
|
|
|
await topOfPage(page)
|
|
|
|
|
await shoot(page, 'handy-freigabe.png')
|
|
|
|
|
await shoot(page, 'handy-freigabe-ganz.png', { fullPage: true })
|
|
|
|
|
|
|
|
|
|
// Jede Schaltfläche und jeder Link der Aktionszeilen: im Fenster, nicht verdeckt, mindestens 40 px hoch (Tippfläche), volle Breite untereinander
|
|
|
|
|
const reach = await page.evaluate(() => {
|
|
|
|
|
const out = []
|
|
|
|
|
for (const el of document.querySelectorAll('.wk-pult-actions > *')) {
|
|
|
|
|
el.scrollIntoView({ block: 'center' })
|
|
|
|
|
const r = el.getBoundingClientRect()
|
|
|
|
|
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
|
|
|
|
|
out.push({ text: el.textContent.trim().slice(0, 28), left: Math.round(r.left), right: Math.round(r.right), height: Math.round(r.height), covered: !(hit && (hit === el || el.contains(hit))) })
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
})
|
|
|
|
|
const bad = reach.filter((r) => r.left < 0 || r.right > 390 || r.covered || r.height < 40)
|
|
|
|
|
check('handy-freigabe: alle Aktionsknöpfe und Links sind erreichbar (im Fenster, nicht verdeckt, mindestens 40 px hoch)', reach.length >= 10 && bad.length === 0, { count: reach.length, bad })
|
|
|
|
|
const w1 = await width()
|
|
|
|
|
check('handy-freigabe: nach dem Durchblättern der Aktionen weiterhin kein seitliches Scrollen', w1.scroll <= w1.inner, w1)
|
|
|
|
|
|
|
|
|
|
// Die lange Adresse steckt in der Originalanfrage: aufgeklappt darf sie die Seite nicht verbreitern
|
|
|
|
|
await rowOf(page, T_MAIL).locator('summary', { hasText: 'Originalanfrage' }).click()
|
|
|
|
|
await sleep(150)
|
|
|
|
|
const w1b = await width()
|
|
|
|
|
check('handy-freigabe: aufgeklappte „Originalanfrage“ mit langer Adresse verbreitert die Seite nicht', w1b.scroll <= w1b.inner, w1b)
|
|
|
|
|
await rowOf(page, T_MAIL).scrollIntoViewIfNeeded()
|
|
|
|
|
await shoot(page, 'handy-freigabe-originalanfrage.png')
|
|
|
|
|
|
|
|
|
|
// Panel am Handy: Bottom-Sheet im Fenster, lange Wörter brechen um, Schließen erreichbar
|
|
|
|
|
const opener = rowOf(page, T_MAIL).getByRole('button', { name: 'Details', exact: true })
|
|
|
|
|
await opener.evaluate((el) => el.setAttribute('data-probe', 'details-opener'))
|
|
|
|
|
await opener.scrollIntoViewIfNeeded()
|
|
|
|
|
await opener.click()
|
|
|
|
|
const dialog = page.getByRole('dialog', { name: 'SV Namborn', exact: true })
|
|
|
|
|
await dialog.waitFor()
|
|
|
|
|
await dialog.locator('.wk-prospect').waitFor()
|
|
|
|
|
await sleep(700)
|
|
|
|
|
const box = await dialog.evaluate((el) => {
|
|
|
|
|
const r = el.getBoundingClientRect()
|
|
|
|
|
const body = el.querySelector('.dialog-body')
|
|
|
|
|
return { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top), bottom: Math.round(r.bottom), inner: window.innerHeight, bodyOverflowX: body.scrollWidth - body.clientWidth, bodyScrolls: body.scrollHeight > body.clientHeight }
|
|
|
|
|
})
|
|
|
|
|
metrics.phonePanel = box
|
|
|
|
|
check('handy-freigabe: Panel liegt im Fenster (links >= 0, rechts <= 390, unten <= Fensterhöhe)', box.left >= 0 && box.right <= 390 && box.top >= 0 && box.bottom <= box.inner, box)
|
|
|
|
|
check('handy-freigabe: Panel ohne waagerechten Überlauf (lange Adressen brechen um)', box.bodyOverflowX <= 0, box)
|
|
|
|
|
const w2 = await width()
|
|
|
|
|
check('handy-freigabe: mit offenem Panel kein seitliches Scrollen', w2.scroll <= w2.inner, w2)
|
|
|
|
|
await shoot(page, 'handy-interessent-panel.png')
|
|
|
|
|
// ganz nach unten im Panel: Nachrichtenverlauf und Schließen-Knopf
|
|
|
|
|
await dialog.locator('.dialog-body').evaluate((el) => { el.scrollTop = el.scrollHeight })
|
|
|
|
|
await sleep(200)
|
|
|
|
|
await shoot(page, 'handy-interessent-panel-unten.png')
|
|
|
|
|
const closeBtn = dialog.getByRole('button', { name: 'Schließen', exact: true }).last()
|
|
|
|
|
const closeReach = await closeBtn.evaluate((el) => {
|
|
|
|
|
const r = el.getBoundingClientRect()
|
|
|
|
|
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
|
|
|
|
|
return { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top), bottom: Math.round(r.bottom), inner: window.innerHeight, covered: !(hit && (hit === el || el.contains(hit))) }
|
|
|
|
|
})
|
|
|
|
|
check('handy-freigabe: „Schließen“ im Panel ist erreichbar (im Fenster, nicht verdeckt)', closeReach.left >= 0 && closeReach.right <= 390 && closeReach.top >= 0 && closeReach.bottom <= closeReach.inner && !closeReach.covered, closeReach)
|
|
|
|
|
const lastMsg = await dialog.locator('li.wk-prospect-msg').last().evaluate((el) => {
|
|
|
|
|
const r = el.getBoundingClientRect()
|
|
|
|
|
const foot = el.closest('.dialog').querySelector('.dialog-foot').getBoundingClientRect()
|
|
|
|
|
return { bottom: Math.round(r.bottom), footTop: Math.round(foot.top) }
|
|
|
|
|
})
|
|
|
|
|
check('handy-freigabe: nach dem Blättern liegt die letzte Nachricht über dem Fuß (nichts abgeschnitten)', lastMsg.bottom <= lastMsg.footTop + 1, lastMsg)
|
|
|
|
|
await closeBtn.click()
|
|
|
|
|
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
|
|
|
|
check('handy-freigabe: „Schließen“ schließt das Panel, Fokus zurück auf „Details“', (await activeProbe(page)) === 'details-opener', await activeTag(page))
|
|
|
|
|
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],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
const server = await serve()
|
|
|
|
|
const base = 'http://127.0.0.1:' + server.address().port
|
|
|
|
|
const browser = await chromium.launch({ executablePath: CHROME, args: ['--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage'] })
|
|
|
|
|
try {
|
|
|
|
|
await desktop(browser, base)
|
|
|
|
|
await bubbles(browser, base)
|
|
|
|
|
await phone(browser, base)
|
|
|
|
|
await reduced(browser, base)
|
|
|
|
|
await halted(browser, base)
|
|
|
|
|
await outage(browser, base)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
problems.push('Abbruch: ' + (e && e.stack ? e.stack : String(e)))
|
|
|
|
|
const only = (process.env.HARNESS_ONLY || '').split(',').map((name) => name.trim()).filter(Boolean)
|
|
|
|
|
for (const [name, run] of SCENARIOS) {
|
|
|
|
|
if (only.length > 0 && !only.includes(name)) continue
|
|
|
|
|
try {
|
|
|
|
|
await run(browser, base)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
problems.push('Abbruch in Szenario ' + name + ': ' + (e && e.stack ? e.stack : String(e)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
await browser.close()
|
|
|
|
|
server.close()
|
|
|
|
|