Workers P4: Prüfstand für das neue Pult – Antworten, Vorschau freigeben, Einfügen, Bauen, Fokus, Handy
Die Skript-API kennt jetzt reply, approve-preview, start-build und paste mit den Prüfreihenfolgen und Sätzen des Dienstes (pultMail.js, paste.js) und einen eigenen Datensatz (?p4=1: Mail-Antwort, zwei Vorschau-Freigaben, unsicher, Kollegen-Eintrag, zwei gesendete Plattform-Antworten desselben Interessenten). Schalter für 400/409/413, Verbindungsabbruch (status 0) und eine Wartezeit je Aktion. Sechs neue Szenarien in shot.cjs: antworten, vorschau, einfuegen, bauen, fokus, handy-p4 (109 Prüfpunkte, Bilder je Zustand). Drei veraltete P3-Prüfpunkte angepasst (seit P4 bietet eine Plattform-Zeile auch „Antwort des Kunden einfügen“). Bricht ein Szenario ab, hält abbruch-<Szenario>.png den Zustand fest. Der Lauf deckt Fehler der App auf (Fokus nach Aktionen, kein Esc/Abbrechen an den Feldern, 32-px-Schließen am Handy); die Prüfpunkte dazu bleiben rot, an der App ist nichts geändert. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ecfc922d4
commit
5138efd2bf
4 files changed
+909
-10
No files matched your search
@@ -7,6 +7,8 @@
|
||||
// ?page=1 zeigt statt des Boards die echte Seite WorkersPage (Reiter Hochhaus und Freigabe, PultList, ProspectPanel); AuthContext,
|
||||
// useEmployees und workersClient sind dort durch stub-*.js ersetzt (build.mjs). ?tab=freigabe öffnet den Reiter gleich (Deep-Link).
|
||||
// Mit ?page=1&fault=none läuft die Seite über den echten API-Client und die Fetch-Weiche (Pult, Aktionen, Interessent).
|
||||
// ?p4=1 (nur mit ?page=1) nimmt den Datensatz für die Gespräche und die Werkstatt (P4: als Mensch antworten, Vorschau freigeben und bauen,
|
||||
// Antwort des Kunden einfügen) statt der P3-Einträge.
|
||||
import { StrictMode, createRef } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
@@ -22,7 +24,7 @@ import { holder } from './harness-bridge.js'
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const pageMode = params.get('page') === '1'
|
||||
const scripted = createScriptedApi({ now: () => performance.now(), base: Date.now(), halted: params.get('halted') === '1', pult: pageMode })
|
||||
const scripted = createScriptedApi({ now: () => performance.now(), base: Date.now(), halted: params.get('halted') === '1', pult: pageMode ? (params.get('p4') === '1' ? 'p4' : true) : false })
|
||||
const stage = createRef()
|
||||
const fault = params.get('fault')
|
||||
const control = { fault: fault && fault !== 'none' ? fault : null, calls: [], jwts: [] }
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// (workers-dev/src/api/anfragenRoutes.js) und verhalten sich wie dessen Regeln (pult.js): übernehmen, als gesendet markieren, verwerfen,
|
||||
// erledigen, 409 mit deutschem Satz bei fremder Übernahme. Die Schalter `faults` lassen einzelne Aktionen mit einem Status und einem
|
||||
// deutschen Satz scheitern; `pultCalls`, `prospectCalls` und `pauseCalls` halten fest, was die Oberfläche geschickt hat.
|
||||
// Gespräche und Werkstatt (P4): pultAction kennt auch reply (als Mensch antworten), approve-preview (Vorschau freigeben), start-build
|
||||
// (Bau starten) und paste (Antwort des Kunden einfügen), mit den Regeln und Sätzen aus workers-dev/src/gespraeche/pultMail.js und
|
||||
// paste.js (Prüfreihenfolge, 400/404/409). Eigener Satz Daten mit `pult: 'p4'` (p4Fixtures); die P3-Daten bleiben unberührt. Die Schalter
|
||||
// `faults.reply`, `.approve-preview`, `.start-build` und `.paste` lassen eine Aktion mit { status, message } scheitern (409 mit deutschem
|
||||
// Satz, 413 „Anfrage zu groß.“, 400), `pultDelayMs` hält jede Pult-Aktion so lange auf (Knöpfe sind währenddessen gesperrt).
|
||||
import { MODEL_IDS, ROLES, SEARCH_ROOMS, roleOf } from '../../src/lib/workersRoster.js'
|
||||
import { indexWorkers, reduceEvents } from '../../src/lib/workersEvents.js'
|
||||
|
||||
@@ -313,6 +318,120 @@ function pultFixtures(base) {
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v))
|
||||
const isoOf = (ms) => new Date(ms).toISOString()
|
||||
|
||||
// ---------- Gespräche und Werkstatt (P4) ----------
|
||||
// Eigener Satz Pult-Einträge für die Szenarien zu „Als Mensch antworten“, „Vorschau freigeben“, „Vorschau bauen“ und „Antwort des Kunden
|
||||
// einfügen“. Nur Felder aus den Whitelists; kein Telefon. Wartet: fünf Einträge (Mail-Antwort, zwei Vorschau-Freigaben, unsicher, ein
|
||||
// Eintrag, den ein Kollege hält). Erledigt: zwei gesendete Plattform-Antworten desselben Interessenten (Standard-Platz und Platz „angebot“),
|
||||
// die Zeilen müssen unabhängig voneinander bleiben.
|
||||
const PREVIEW_OK = 'https://entwurf.project.webklar.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/'
|
||||
const PREVIEW_BAD = 'http://entwurf.project.webklar.com/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/' // kein https: darf kein Link werden
|
||||
const REPLY_TEXT_MAX = 8000
|
||||
const REPLY_ACTIONS = ['reply', 'approve-preview', 'start-build', 'paste']
|
||||
|
||||
function p4Fixtures(base) {
|
||||
const at = (ms) => isoOf(base + ms)
|
||||
const items = [
|
||||
{
|
||||
$id: 'p4-mail',
|
||||
kind: 'mail_antwort',
|
||||
prospectId: 'p4-mail',
|
||||
threadId: 'thr-p4-mail',
|
||||
title: 'Anfrage: Vereinswebsite für den SV Namborn',
|
||||
originalText: 'Hallo,\nwir suchen jemanden, der unsere Vereinsseite neu macht.',
|
||||
draft: 'Guten Tag Herr Beispiel,\n\nvielen Dank für Ihre Anfrage. Wir bauen Ihnen kostenlos eine erste Vorschau Ihrer neuen Vereinsseite.\n\nBeste Grüße\nKenso',
|
||||
status: 'offen',
|
||||
createdAt: at(-1 * HOUR_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-freigabe',
|
||||
kind: 'vorschau_freigabe',
|
||||
prospectId: 'p4-vorschau',
|
||||
previewId: 'prev-p4',
|
||||
title: 'Vorschau freigeben: Friseur Muster',
|
||||
draft: 'Guten Tag Frau Muster,\n\nhier ist Ihre erste Vorschau.',
|
||||
sourceUrl: PREVIEW_OK,
|
||||
status: 'offen',
|
||||
createdAt: at(-2 * HOUR_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-freigabe-falsch',
|
||||
kind: 'vorschau_freigabe',
|
||||
prospectId: 'p4-vorschau2',
|
||||
previewId: 'prev-p4-2',
|
||||
title: 'Vorschau freigeben: Bäckerei Korn',
|
||||
draft: 'Guten Tag, hier ist Ihre erste Vorschau.',
|
||||
sourceUrl: PREVIEW_BAD,
|
||||
status: 'offen',
|
||||
createdAt: at(-3 * HOUR_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-unsicher',
|
||||
kind: 'unsicher',
|
||||
prospectId: 'p4-unsicher',
|
||||
title: 'Unsichere Antwort: Ist das ein Ja?',
|
||||
originalText: 'Nora war sich bei dieser Antwort nicht sicher: „Ja, aber erst nach dem Urlaub, vielleicht.“',
|
||||
status: 'offen',
|
||||
createdAt: at(-4 * HOUR_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-kollege',
|
||||
kind: 'mail_antwort',
|
||||
prospectId: 'p4-kollege',
|
||||
title: 'Anfrage: Tischlerei Holz',
|
||||
draft: 'Guten Tag, wir melden uns mit einer Vorschau.',
|
||||
status: 'in_arbeit',
|
||||
claimedBy: COLLEAGUE,
|
||||
claimedAt: at(-1 * HOUR_MS),
|
||||
createdAt: at(-5 * HOUR_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-plat-gesendet',
|
||||
kind: 'plattform_antwort',
|
||||
prospectId: 'p4-plat',
|
||||
title: 'Kleinanzeigen: Website für Fotostudio',
|
||||
draft: 'Hallo, wir bauen Ihnen gern eine erste Vorschau.',
|
||||
sourceUrl: FOTO_URL,
|
||||
status: 'gesendet',
|
||||
claimedBy: ME,
|
||||
claimedAt: at(-2 * DAY_MS + HOUR_MS),
|
||||
doneBy: ME,
|
||||
doneAt: at(-1 * DAY_MS),
|
||||
createdAt: at(-2 * DAY_MS),
|
||||
},
|
||||
{
|
||||
$id: 'p4-plat-angebot',
|
||||
kind: 'plattform_antwort',
|
||||
prospectId: 'p4-plat',
|
||||
title: 'Kleinanzeigen: Website für Fotostudio (Angebot)',
|
||||
draft: 'Hallo, hier ist das Angebot zu Ihrer Vorschau.',
|
||||
sourceUrl: FOTO_URL,
|
||||
status: 'gesendet',
|
||||
claimedBy: ME,
|
||||
claimedAt: at(-4 * HOUR_MS),
|
||||
doneBy: ME,
|
||||
doneAt: at(-3 * HOUR_MS),
|
||||
createdAt: at(-1 * DAY_MS),
|
||||
},
|
||||
]
|
||||
const common = { entrance: 'anfrage', room: 'anfragen', foundBy: 'rita' }
|
||||
const prospects = {
|
||||
'p4-mail': { ...common, source: 'freelancermap-alarm', sourceUrl: SV_URL, foundAt: at(-3 * HOUR_MS), name: 'SV Namborn', contactName: 'Hans Beispiel', email: 'vorstand@sv-namborn.example', city: 'Namborn', postalCode: '66640', score: 84, status: 'bewertet', threadId: 'thr-p4-mail' },
|
||||
'p4-vorschau': { ...common, source: 'freelancermap-alarm', foundAt: at(-1 * DAY_MS), name: 'Friseur Muster', email: 'info@friseur-muster.example', city: 'Kaiserslautern', postalCode: '67655', score: 80, status: 'vorschau_bau', previewId: 'prev-p4' },
|
||||
'p4-vorschau2': { ...common, source: 'freelancermap-alarm', foundAt: at(-1 * DAY_MS), name: 'Bäckerei Korn', email: 'info@baeckerei-korn.example', city: 'Landstuhl', postalCode: '66849', score: 75, status: 'vorschau_bau', previewId: 'prev-p4-2' },
|
||||
'p4-unsicher': { ...common, source: 'freelancermap-alarm', foundAt: at(-2 * DAY_MS), name: 'Gasthaus Sonne', email: 'wirt@gasthaus-sonne.example', city: 'Pirmasens', postalCode: '66953', score: 70, status: 'gespraech' },
|
||||
'p4-kollege': { ...common, source: 'freelancermap-alarm', foundAt: at(-1 * DAY_MS), name: 'Tischlerei Holz', email: 'holz@tischlerei-holz.example', city: 'Zweibrücken', postalCode: '66482', status: 'gespraech' },
|
||||
'p4-plat': { ...common, source: 'kleinanzeigen-alarm', sourceUrl: FOTO_URL, foundAt: at(-3 * DAY_MS), name: 'Foto Lichtblick', city: 'Mainz', postalCode: '55116', score: 78, status: 'kontaktiert' },
|
||||
}
|
||||
const threads = {
|
||||
'thr-p4-mail': { $id: 'thr-p4-mail', prospectId: 'p4-mail', channel: 'mail', toEmail: 'vorstand@sv-namborn.example', subject: 'Re: SV Namborn', status: 'wartet_auf_uns', followupSent: false, reminderSent: false, createdAt: at(-3 * HOUR_MS) },
|
||||
}
|
||||
const previews = {
|
||||
'prev-p4': { $id: 'prev-p4', prospectId: 'p4-vorschau', builder: 'bob', status: 'freigabe', round: 1, createdAt: at(-4 * HOUR_MS) },
|
||||
'prev-p4-2': { $id: 'prev-p4-2', prospectId: 'p4-vorschau2', builder: 'bob', status: 'freigabe', round: 1, createdAt: at(-5 * HOUR_MS) },
|
||||
}
|
||||
return { items, prospects, threads, messages: {}, previews }
|
||||
}
|
||||
|
||||
// Standardwerte der Einstellungen (Übersicht §4.1)
|
||||
function defaultSettings(halted) {
|
||||
return {
|
||||
@@ -352,7 +471,8 @@ function defaultSettings(halted) {
|
||||
/**
|
||||
* Skript-API für den Prüfstand. now() = Uhr in Millisekunden (nur die Differenz zum Aufruf von createScriptedApi zählt),
|
||||
* base = Zeitpunkt (ms) des Starts für die Zeitstempel der Ereignisse, halted = Bounce-Notbremse ist von Anfang an gesetzt (H4),
|
||||
* pult = das Freigabe-Pult und die Interessenten (P3) mit den festen Daten füllen (sonst ist das Pult leer).
|
||||
* pult = das Freigabe-Pult und die Interessenten (P3) mit den festen Daten füllen (sonst ist das Pult leer); 'p4' nimmt statt dessen den
|
||||
* Satz für die Gespräche und die Werkstatt (p4Fixtures).
|
||||
* Rückgabe wie createWorkersApi; saved ist die Liste der Patches, die saveSettings bekam.
|
||||
*/
|
||||
export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pult = false }) {
|
||||
@@ -361,9 +481,12 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
let paused = false
|
||||
const settings = defaultSettings(halted === true)
|
||||
const saved = []
|
||||
const fx = pult ? pultFixtures(base) : { items: [], prospects: {}, threads: {}, messages: {}, previews: {} } // ohne `pult` ist das Pult leer wie bisher
|
||||
// ohne `pult` ist das Pult leer wie bisher
|
||||
const fx = pult === 'p4' ? p4Fixtures(base) : pult ? pultFixtures(base) : { items: [], prospects: {}, threads: {}, messages: {}, previews: {} }
|
||||
const pultItems = fx.items
|
||||
const faults = { pult: null, claim: null, sent: null, discard: null, done: null, prospect: null } // je { status, message } oder null
|
||||
// je { status, message } oder null; die vier Schlüssel mit Bindestrich oder Namen der P4-Aktionen stehen ebenfalls hier
|
||||
const faults = { pult: null, claim: null, sent: null, discard: null, done: null, prospect: null, reply: null, 'approve-preview': null, 'start-build': null, paste: null }
|
||||
const pastedBefore = new Set() // Interessent + Text: dieselbe eingefügte Antwort ist beim zweiten Mal „doppelt“
|
||||
const pultCalls = []
|
||||
const prospectCalls = []
|
||||
const pauseCalls = []
|
||||
@@ -417,6 +540,81 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
const nowIsoString = () => isoOf(base + elapsed())
|
||||
const shortOf = (id) => (Object.hasOwn(fx.prospects, id) ? { $id: id, ...pick(fx.prospects[id], PROSPECT_SHORT) } : null)
|
||||
|
||||
// ---- P4: reply, approve-preview, start-build, paste (Regeln und Sätze wie pultMail.js und paste.js des Dienstes) ----
|
||||
const TOT = ['verloren', 'deal'] // gesperrt hat seinen eigenen Satz
|
||||
const conflict = (message) => httpError(409, message)
|
||||
const NO_PROSPECT = 'Zu diesem Eintrag gibt es keinen Interessenten.'
|
||||
const checkEditable = (item, editable) => {
|
||||
if (!editable) throw conflict('Schon erledigt.')
|
||||
if (item.status === 'in_arbeit' && item.claimedBy !== ME) throw conflict('Wird schon von einem Kollegen bearbeitet.')
|
||||
}
|
||||
const prospectOf = (item) => (Object.hasOwn(fx.prospects, item.prospectId) ? fx.prospects[item.prospectId] : null)
|
||||
const needProspect = (item) => {
|
||||
const prospect = prospectOf(item)
|
||||
if (prospect === null) throw conflict(NO_PROSPECT)
|
||||
return prospect
|
||||
}
|
||||
const checkAlive = (prospect) => {
|
||||
if (prospect.status === 'gesperrt') throw conflict('Dieser Kontakt ist gesperrt.')
|
||||
if (TOT.includes(prospect.status)) throw conflict('Dieser Kontakt ist nicht mehr aktiv.')
|
||||
}
|
||||
const close = (item, status, now) => Object.assign(item, { status, doneBy: ME, doneAt: now })
|
||||
// Text einer Antwort: getrimmt, nicht leer, höchstens 8000 Zeichen (Code Points)
|
||||
const replyText = (text, emptyMessage, wrongTypeMessage) => {
|
||||
if (text !== undefined && text !== null && typeof text !== 'string' && wrongTypeMessage) throw httpError(400, wrongTypeMessage)
|
||||
const value = typeof text === 'string' ? text.trim() : ''
|
||||
if (value === '') throw httpError(400, emptyMessage)
|
||||
if (Array.from(value).length > REPLY_TEXT_MAX) throw httpError(400, `Der Text ist zu lang (höchstens ${REPLY_TEXT_MAX} Zeichen).`)
|
||||
return value
|
||||
}
|
||||
|
||||
function p4Action(item, action, body, editable, now) {
|
||||
const input = body !== null && typeof body === 'object' ? body : {}
|
||||
if (action === 'reply') {
|
||||
if (!['mail_antwort', 'unsicher', 'preisfrage', 'mensch_gewuenscht'].includes(item.kind)) throw conflict('Dazu gibt es keine Antwort-Funktion.')
|
||||
if (!item.prospectId) throw conflict(NO_PROSPECT)
|
||||
checkEditable(item, editable)
|
||||
replyText(input.text, 'Der Text der Antwort fehlt.', 'Der Text der Antwort muss ein Text sein.')
|
||||
const prospect = needProspect(item)
|
||||
if (prospect.status === 'gesperrt') throw conflict('Dieser Kontakt ist gesperrt.')
|
||||
const thread = prospect.threadId && Object.hasOwn(fx.threads, prospect.threadId) ? fx.threads[prospect.threadId] : null
|
||||
if (thread !== null ? thread.channel !== 'mail' : !prospect.email) throw conflict('Nur für Mail-Fäden.')
|
||||
return clone({ item: close(item, 'gesendet', now) })
|
||||
}
|
||||
if (action === 'approve-preview') {
|
||||
if (item.kind !== 'vorschau_freigabe') throw conflict('Dieser Eintrag ist keine Vorschau-Freigabe.')
|
||||
if (!item.prospectId) throw conflict(NO_PROSPECT)
|
||||
checkEditable(item, editable)
|
||||
const preview = item.previewId && Object.hasOwn(fx.previews, item.previewId) ? fx.previews[item.previewId] : null
|
||||
if (preview === null || preview.status !== 'freigabe' || preview.convertedProjectId) throw conflict('Die Vorschau ist nicht freigabebereit.')
|
||||
checkAlive(needProspect(item))
|
||||
return clone({ item: close(item, 'erledigt', now), previewUrl: item.sourceUrl ?? null })
|
||||
}
|
||||
if (action === 'start-build') {
|
||||
if (item.kind !== 'unsicher') throw conflict('Dazu gibt es keine Bau-Funktion.')
|
||||
if (!item.prospectId) throw conflict(NO_PROSPECT)
|
||||
checkEditable(item, editable)
|
||||
const prospect = needProspect(item)
|
||||
checkAlive(prospect)
|
||||
if (prospect.previewId) throw conflict('Für diesen Interessenten gibt es schon eine Vorschau.')
|
||||
prospect.status = 'vorschau_bau'
|
||||
return clone({ item: close(item, 'erledigt', now) })
|
||||
}
|
||||
// paste: Text, Eintrag (daraus der Interessent), Interessent, Faden-Kanal, Sperre; Antwort { status, messageId, threadId }
|
||||
const text = replyText(input.text, 'Bitte einen Text eingeben.', 'Bitte einen Text eingeben.')
|
||||
if (!item.prospectId) throw conflict(NO_PROSPECT)
|
||||
const prospect = prospectOf(item)
|
||||
if (prospect === null) throw httpError(404, 'Interessent nicht gefunden.')
|
||||
const thread = prospect.threadId && Object.hasOwn(fx.threads, prospect.threadId) ? fx.threads[prospect.threadId] : null
|
||||
if (thread !== null && thread.channel !== 'plattform') throw conflict('Nur für Plattform-Fäden.')
|
||||
if (thread === null && prospect.entrance !== 'anfrage') throw conflict('Nur für Plattform-Anfragen.')
|
||||
if (prospect.status === 'gesperrt') throw conflict('Dieser Kontakt ist gesperrt.')
|
||||
const key = `${item.prospectId}:${text.replace(/\s+/g, ' ')}`
|
||||
const status = pastedBefore.has(key) ? 'doppelt' : 'neu'
|
||||
pastedBefore.add(key)
|
||||
return clone({ status, messageId: `msg-p4-${pastedBefore.size}`, threadId: `thr-p4-${item.prospectId}` })
|
||||
}
|
||||
|
||||
const api = {
|
||||
saved,
|
||||
faults,
|
||||
@@ -424,6 +622,7 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
prospectCalls,
|
||||
pauseCalls,
|
||||
pultEmpty: false, // true: das Pult ist leer (Leerzustand)
|
||||
pultDelayMs: 0, // > 0: jede Pult-Aktion antwortet erst nach so vielen Millisekunden (Knöpfe sind in der Zwischenzeit gesperrt)
|
||||
|
||||
async state() {
|
||||
return clone(buildState())
|
||||
@@ -491,7 +690,8 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
// POST /pult/:id/<claim|sent|discard|done>: Regeln wie pult.js (claimPult, markSent, discard, donePult); Antwort { item }
|
||||
async pultAction(id, action, body) {
|
||||
pultCalls.push({ id, action, body: body === undefined ? undefined : clone(body) })
|
||||
if (!['claim', 'sent', 'discard', 'done'].includes(action)) throw httpError(404, 'Unbekannte Aktion.')
|
||||
if (!['claim', 'sent', 'discard', 'done', ...REPLY_ACTIONS].includes(action)) throw httpError(404, 'Unbekannte Aktion.')
|
||||
if (api.pultDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, api.pultDelayMs))
|
||||
if (faults[action]) throw failure(faults[action])
|
||||
const item = pultItems.find((entry) => entry.$id === id)
|
||||
if (!item) throw httpError(404, 'Eintrag nicht gefunden.')
|
||||
@@ -499,6 +699,8 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
const prospect = Object.hasOwn(fx.prospects, item.prospectId) ? fx.prospects[item.prospectId] : null
|
||||
const now = nowIsoString()
|
||||
|
||||
if (REPLY_ACTIONS.includes(action)) return p4Action(item, action, body, editable, now)
|
||||
|
||||
if (action === 'claim') {
|
||||
if (item.status === 'in_arbeit') {
|
||||
if (item.claimedBy !== ME) throw httpError(409, 'Wird schon von einem Kollegen bearbeitet.')
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// Ausfälle lassen sich schalten: control.fault = null (gesund) | 'down' (503 ohne JSON wie eine Fehlerseite des Proxys) |
|
||||
// 'session' (401) | 'hang' (die Antwort kommt nie, wie auf einer toten Verbindung). Kein Netz, kein Appwrite.
|
||||
// Seit P3 auch Pult (GET /pult, POST /pult/:id/<Aktion>) und Interessent (GET /prospect/:id): Fehler der Skript-API (status + deutscher
|
||||
// Satz) kommen als { error } mit diesem Status an, wie vom Dienst.
|
||||
// Satz) kommen als { error } mit diesem Status an, wie vom Dienst (auch 413 „Anfrage zu groß.“). Seit P4 laufen reply, approve-preview,
|
||||
// start-build und paste über dieselbe Route POST /pult/:id/<Aktion>. Ein Fehler mit status 0 aus der Skript-API bricht die Verbindung ab
|
||||
// (fetch wirft), damit die Meldung „Kann gesendet sein – Pult neu laden.“ (Zeitüberschreitung, D14) sichtbar wird.
|
||||
// control.calls sammelt jede Anfrage ({ method, path, jwt }), damit der Prüfstand zählen kann.
|
||||
|
||||
const reply = (status, body) => ({
|
||||
@@ -44,6 +46,8 @@ export function createScriptedFetch(api, control) {
|
||||
if (method === 'GET' && prospect) return reply(200, await api.prospect(decodeURIComponent(prospect[1])))
|
||||
return reply(404, { error: 'Unbekannter Pfad im Prüfstand.' })
|
||||
} catch (err) {
|
||||
// Status 0 aus der Skript-API = die Verbindung bricht ab, bevor eine Antwort da ist (fetch wirft wie bei einem Netzfehler)
|
||||
if (err && err.status === 0) throw new TypeError('Failed to fetch')
|
||||
return reply(err && err.status ? err.status : 500, { error: err && err.message ? err.message : 'Fehler im Prüfstand.' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// <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
|
||||
// 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
|
||||
// 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'
|
||||
@@ -71,6 +73,7 @@ function check(name, ok, detail) {
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
let lastPage = null // die zuletzt geöffnete Seite: bricht ein Szenario ab, wird von hier aus der Zustand im Bild festgehalten (abbruch-<Szenario>.png)
|
||||
const textOf = async (loc) => ((await loc.textContent()) || '').replace(/\s+/g, ' ').trim()
|
||||
|
||||
async function shoot(page, name, options = {}) {
|
||||
@@ -96,6 +99,7 @@ async function newPage(browser, label, contextOptions) {
|
||||
window.cancelAnimationFrame = (id) => { waiting.delete(id); caf(id) }
|
||||
})
|
||||
const page = await context.newPage()
|
||||
lastPage = page
|
||||
page.setDefaultTimeout(20000)
|
||||
page.on('pageerror', (e) => errors.push(label + ' pageerror: ' + String(e)))
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(label + ' console.error: ' + m.text()) })
|
||||
@@ -584,7 +588,7 @@ async function freigabe(browser, base) {
|
||||
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: ${T_PLAT}`, 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))
|
||||
check('freigabe: Plattform-Zeile (offen): Kopieren & öffnen, Ich kümmere mich, Als gesendet markieren, Antwort des Kunden einfügen (P4), Verwerfen', same(await buttonNames(plat), ['Kopieren & öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Antwort des Kunden einfügen', '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)
|
||||
@@ -700,7 +704,7 @@ async function freigabe(browser, base) {
|
||||
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)))
|
||||
check('freigabe: veralteter Eintrag lässt sich weiter bearbeiten (Kopieren & öffnen, Ich kümmere mich, Als gesendet markieren, Antwort des Kunden einfügen, Verwerfen)', same(await buttonNames(rowOf(page, T_ALT)), ['Kopieren & öffnen', 'Ich kümmere mich', 'Als gesendet markieren', 'Antwort des Kunden einfügen', '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()
|
||||
@@ -710,7 +714,14 @@ async function freigabe(browser, base) {
|
||||
|
||||
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)
|
||||
// Seit P4 nimmt eine gesendete Plattform-Antwort auch danach noch die Antwort des Kunden an: dort steht der eine Knopf „Antwort des Kunden einfügen“;
|
||||
// verworfene und erledigte Zeilen sowie die gesendete Mail-Antwort haben keine Aktionen mehr
|
||||
const doneActions = await page.evaluate(() => [...document.querySelectorAll('.wk-pult-row')].map((row) => ({
|
||||
title: row.querySelector('.wk-pult-title').textContent,
|
||||
actions: [...row.querySelectorAll('.wk-pult-actions > *')].map((el) => el.textContent.trim()),
|
||||
})))
|
||||
check('freigabe: „Erledigt“ zeigt gesendet, verworfen, erledigt (fünf Zeilen), Aktion nur „Antwort des Kunden einfügen“ an den gesendeten Plattform-Antworten',
|
||||
doneActions.length === 5 && doneActions.every((r) => same(r.actions, [T_PLAT, T_GES].includes(r.title) ? ['Antwort des Kunden einfügen'] : [])), doneActions)
|
||||
const doneMeta = await page.evaluate(() => [...document.querySelectorAll('.wk-pult-row')].map((row) => ({
|
||||
badge: [...row.querySelectorAll('.wk-pult-head .badge')].map((b) => b.textContent.trim()).filter((t) => ['Gesendet', 'Verworfen', 'Erledigt'].includes(t)),
|
||||
meta: row.querySelector('.wk-pult-meta').textContent.trim(),
|
||||
@@ -982,9 +993,688 @@ async function handyFreigabe(browser, base) {
|
||||
}
|
||||
|
||||
|
||||
// ---------- P4: Gespräche und Werkstatt (Pult: als Mensch antworten, Vorschau freigeben und bauen, Antwort des Kunden einfügen) ----------
|
||||
// Eigener Datensatz (?p4=1, scripted-api.js p4Fixtures). Wartet: Mail-Antwort, zwei Vorschau-Freigaben (eine mit gültiger, eine mit falscher
|
||||
// Vorschau-Adresse), „unsicher“ und ein Eintrag, den ein Kollege hält (fünf Zeilen). Erledigt: zwei gesendete Plattform-Antworten desselben
|
||||
// Interessenten (Standard-Platz und Platz „angebot“). Die Schalter der Skript-API (faults.reply, .approve-preview, .start-build, .paste und
|
||||
// pultDelayMs) steuern 400/409/413 und die Wartezeit; pultCalls hält fest, was die Oberfläche geschickt hat.
|
||||
|
||||
const P4_URL = '/index.html?page=1&fault=none&p4=1'
|
||||
const T4 = {
|
||||
mail: 'Anfrage: Vereinswebsite für den SV Namborn',
|
||||
frei: 'Vorschau freigeben: Friseur Muster',
|
||||
falsch: 'Vorschau freigeben: Bäckerei Korn',
|
||||
uns: 'Unsichere Antwort: Ist das ein Ja?',
|
||||
koll: 'Anfrage: Tischlerei Holz',
|
||||
plat: 'Kleinanzeigen: Website für Fotostudio',
|
||||
ang: 'Kleinanzeigen: Website für Fotostudio (Angebot)',
|
||||
}
|
||||
const PREVIEW_URL = 'https://entwurf.project.webklar.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/'
|
||||
const GONE = 'Dieser Kontakt ist nicht mehr aktiv.'
|
||||
const TOO_BIG = 'Anfrage zu groß.'
|
||||
const NEED_TEXT = 'Bitte einen Text eingeben.'
|
||||
const TOO_LONG_8000 = 'Der Text ist zu lang (höchstens 8000 Zeichen).'
|
||||
const PASTE_HINT = 'Kopiere die Antwort aus der Plattform hierher. Nora sortiert sie ein; bei einer Frage schreibt Clara den nächsten Entwurf.'
|
||||
const PASTE_OK = 'Eingefügt. Nora sortiert die Antwort ein.'
|
||||
const PASTE_TWICE = 'Diese Antwort war schon eingefügt.'
|
||||
const NO_PREVIEW_URL = 'Keine gültige Vorschau-Adresse hinterlegt.'
|
||||
|
||||
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
// Zeile mit genau diesem Titel (rowOf sucht nur einen Teil des Titels: „… (Angebot)“ träfe sonst auch die Zeile davor)
|
||||
const row4 = (page, title) => page.locator('li.wk-pult-row').filter({ has: page.locator('.wk-pult-title', { hasText: new RegExp('^' + escapeRe(title) + '$') }) })
|
||||
const actionNames = (row) => row.locator('.wk-pult-actions').locator('button, a').allTextContents().then((list) => list.map((t) => t.replace(/\s+/g, ' ').trim()))
|
||||
const callsOf = async (page, action, id) => (await pultCalls(page)).filter((c) => c.action === action && (id === undefined || c.id === id))
|
||||
const setDelay = (page, ms) => page.evaluate((v) => { window.__wk.api.pultDelayMs = v }, ms)
|
||||
const noteOf = (row) => row.locator('.msg')
|
||||
const focusInfo = (page) => page.evaluate(() => {
|
||||
const el = document.activeElement
|
||||
if (!el) return { tag: null }
|
||||
return {
|
||||
tag: el.tagName,
|
||||
label: el.getAttribute('aria-label'),
|
||||
probe: el.getAttribute('data-probe'),
|
||||
text: (el.textContent || '').trim().slice(0, 30),
|
||||
inReply: Boolean(el.closest('.wk-pult-reply')),
|
||||
inPaste: Boolean(el.closest('.wk-pult-paste')),
|
||||
inDialog: Boolean(el.closest('[role="dialog"]')),
|
||||
}
|
||||
})
|
||||
const phoneFree = async (page) => {
|
||||
const html = await page.evaluate(() => document.documentElement.outerHTML)
|
||||
const text = await page.evaluate(() => document.body.innerText)
|
||||
return !PHONE_WORD.test(html) && !PHONE_NUMBER.test(html) && !PHONE_WORD.test(text) && !PHONE_NUMBER.test(text)
|
||||
}
|
||||
const widthOf = (page) => page.evaluate(() => ({ scroll: document.scrollingElement.scrollWidth, inner: window.innerWidth }))
|
||||
// Schaltflächen und Links: im Fenster, nicht verdeckt, Höhe in px (Tippfläche)
|
||||
const reachOf = (page, selector) => page.evaluate((sel) => {
|
||||
const out = []
|
||||
for (const el of document.querySelectorAll(sel)) {
|
||||
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 || el.getAttribute('aria-label') || '').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
|
||||
}, selector)
|
||||
const badReach = (list) => list.filter((r) => r.left < 0 || r.right > 390 || r.covered || r.height < 40)
|
||||
|
||||
// Reiter „Freigabe“ mit dem P4-Datensatz: fünf wartende Zeilen und die Mitarbeiternamen sind da
|
||||
async function openP4(page, base) {
|
||||
await page.goto(base + P4_URL + '&tab=freigabe')
|
||||
await page.waitForFunction(() => document.querySelectorAll('li.wk-pult-row').length === 5)
|
||||
await page.waitForFunction(() => document.body.textContent.includes('übernommen von Justin'))
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
}
|
||||
const chooseFilter4 = (page, label) => page.locator('.wk-pult-filters .seg button', { hasText: label }).click()
|
||||
async function openP4Done(page, base) {
|
||||
await openP4(page, base)
|
||||
await chooseFilter4(page, 'Erledigt')
|
||||
await waitTitles(page, [T4.ang, T4.plat])
|
||||
}
|
||||
|
||||
// ---------- Szenario K: „Als Mensch antworten“ (Desktop 1280 x 900) ----------
|
||||
|
||||
async function antworten(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'antworten', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await openP4(page, base)
|
||||
const mail = row4(page, T4.mail)
|
||||
const uns = row4(page, T4.uns)
|
||||
const koll = row4(page, T4.koll)
|
||||
const draft = (await page.evaluate(() => window.__wk.api.pult('offen,in_arbeit'))).items.find((i) => i.$id === 'p4-mail').draft
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'antworten-wartet.png')
|
||||
|
||||
check('antworten: wartende Zeilen neueste zuerst (Mail, Freigabe, Freigabe mit falscher Adresse, unsicher, Kollege)', same(await rowTitles(page), [T4.mail, T4.frei, T4.falsch, T4.uns, T4.koll]), await rowTitles(page))
|
||||
check('antworten: Mail-Zeile: E-Mail schreiben, Ich kümmere mich, Als gesendet markieren, Als Mensch antworten, Verwerfen', same(await actionNames(mail), ['E-Mail schreiben', 'Ich kümmere mich', 'Als gesendet markieren', 'Als Mensch antworten', 'Verwerfen']), await actionNames(mail))
|
||||
check('antworten: Kollegen-Zeile (von Justin übernommen) bietet kein „Als Mensch antworten“', (await koll.getByRole('button', { name: 'Als Mensch antworten' }).count()) === 0, await actionNames(koll))
|
||||
|
||||
// 1. Aufklappen: vorbelegt mit dem Entwurf, Zähler, eigener Name des Felds
|
||||
const opener = mail.getByRole('button', { name: 'Als Mensch antworten', exact: true })
|
||||
check('antworten: vor dem Öffnen kein Antwortfeld, Knopf mit aria-expanded=false', (await mail.locator('.wk-pult-reply').count()) === 0 && (await opener.getAttribute('aria-expanded')) === 'false')
|
||||
await opener.click()
|
||||
const panel = mail.locator('.wk-pult-reply')
|
||||
await panel.waitFor()
|
||||
const area = panel.locator('textarea')
|
||||
const count = () => textOf(panel.locator('.wk-pult-count'))
|
||||
const send = panel.getByRole('button', { name: 'Jetzt senden', exact: true })
|
||||
check('antworten: „Als Mensch antworten“ klappt das Feld auf (aria-expanded=true), Textfeld heißt „Text der Antwort: …“', (await opener.getAttribute('aria-expanded')) === 'true' && (await area.getAttribute('aria-label')) === `Text der Antwort: ${T4.mail}`, await area.getAttribute('aria-label'))
|
||||
check('antworten: das Feld ist mit dem Entwurf vorbelegt', (await area.inputValue()) === draft)
|
||||
check(`antworten: Zähler zeigt die Zeichen des Entwurfs („${Array.from(draft.trim()).length} / 8000“)`, (await count()) === `${Array.from(draft.trim()).length} / 8000`, await count())
|
||||
check('antworten: Knopf „Jetzt senden“ im Fuß des Felds', (await send.count()) === 1)
|
||||
await shoot(page, 'antworten-offen-vorbelegt.png')
|
||||
|
||||
// 2. Tippen: der Zähler zählt Zeichen (Code Points), der Entwurf in der Zeile bleibt unberührt
|
||||
await area.fill('Hallo 😀')
|
||||
check('antworten: Zähler zählt mit (ein Emoji ist ein Zeichen: „7 / 8000“)', (await count()) === '7 / 8000', await count())
|
||||
check('antworten: das Antwortfeld ändert den Entwurf der Zeile nicht (getrennte Felder)', (await mail.getByLabel(`Entwurf der Antwort: ${T4.mail}`, { exact: true }).inputValue()) === draft)
|
||||
|
||||
// 3. Leerer Text: Fehler in der Oberfläche, kein Aufruf am Dienst
|
||||
await area.fill('')
|
||||
await send.click()
|
||||
const err = mail.locator('.msg.err')
|
||||
await err.waitFor()
|
||||
check('antworten: leerer Text ⇒ „Bitte einen Text eingeben.“ (rot, role=alert), kein Aufruf am Dienst', (await textOf(err)) === NEED_TEXT && (await err.getAttribute('role')) === 'alert' && (await callsOf(page, 'reply')).length === 0, { text: await textOf(err), calls: (await callsOf(page, 'reply')).length })
|
||||
await shoot(page, 'antworten-leer-fehler.png')
|
||||
await area.fill(' \n \t ')
|
||||
await send.click()
|
||||
check('antworten: nur Leerraum ⇒ derselbe Satz, weiterhin kein Aufruf', (await textOf(err)) === NEED_TEXT && (await callsOf(page, 'reply')).length === 0)
|
||||
|
||||
// 4. Zu lang: 8001 Zeichen (Emoji zählen als eins) ⇒ Fehler, Zähler rot; genau 8000 Emoji sind erlaubt
|
||||
await area.fill('😀'.repeat(8001))
|
||||
check('antworten: 8001 Zeichen: Zähler „8001 / 8000“ ist rot (Klasse over)', (await count()) === '8001 / 8000' && (await panel.locator('.wk-pult-count').getAttribute('class')).includes('over'), await count())
|
||||
await send.click()
|
||||
await page.waitForFunction((t) => document.querySelector('li.wk-pult-row .msg.err') && document.querySelector('li.wk-pult-row .msg.err').textContent === t, TOO_LONG_8000)
|
||||
check('antworten: 8001 Zeichen ⇒ „Der Text ist zu lang (höchstens 8000 Zeichen).“, kein Aufruf am Dienst', (await callsOf(page, 'reply')).length === 0)
|
||||
await shoot(page, 'antworten-zu-lang.png')
|
||||
await area.fill('😀'.repeat(8000))
|
||||
check('antworten: genau 8000 Emoji (16000 UTF-16-Einheiten): Zähler „8000 / 8000“ ohne Rot', (await count()) === '8000 / 8000' && !(await panel.locator('.wk-pult-count').getAttribute('class')).includes('over'), await count())
|
||||
|
||||
// 5. 409 vom Dienst: der Satz steht in der Zeile, der Text bleibt, die Zeile bleibt, der Knopf ist wieder frei
|
||||
const answer = ' Guten Tag Herr Beispiel,\n\nanbei die Vorschau, wie besprochen.\n\nBeste Grüße\nKenso \n'
|
||||
await area.fill(answer)
|
||||
await setPultFault(page, 'reply', { status: 409, message: CONFLICT })
|
||||
await send.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), CONFLICT)
|
||||
check('antworten: 409 ⇒ der Satz des Dienstes steht in der Zeile (rot, role=alert)', (await textOf(noteOf(mail))) === CONFLICT && (await noteOf(mail).getAttribute('role')) === 'alert', await textOf(noteOf(mail)))
|
||||
check('antworten: nach dem 409 bleiben Zeile, Text und Feld erhalten, „Jetzt senden“ ist wieder frei', (await mail.count()) === 1 && (await area.inputValue()) === answer && (await send.isEnabled()) && (await mail.getByRole('button', { name: 'Verwerfen' }).isEnabled()))
|
||||
await shoot(page, 'antworten-409.png')
|
||||
await setPultFault(page, 'reply', { status: 409, message: GONE })
|
||||
await send.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), GONE)
|
||||
check('antworten: zweiter 409 ersetzt die Meldung („Dieser Kontakt ist nicht mehr aktiv.“), nicht angehäuft', (await noteOf(mail).count()) === 1 && (await textOf(noteOf(mail))) === GONE)
|
||||
await setPultFault(page, 'reply', { status: 413, message: TOO_BIG })
|
||||
await send.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), TOO_BIG)
|
||||
check('antworten: 413 ⇒ „Anfrage zu groß.“ in der Zeile, Text bleibt', (await textOf(noteOf(mail))) === TOO_BIG && (await area.inputValue()) === answer)
|
||||
|
||||
// Verbindung bricht beim Senden ab: ob die Mail hinausging, ist offen (D14), die Meldung sagt das und schickt nicht zum zweiten Mal
|
||||
await setPultFault(page, 'reply', { status: 0, message: 'Verbindung weg' })
|
||||
await send.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), 'Kann gesendet sein – Pult neu laden.')
|
||||
check('antworten: Verbindungsabbruch beim Senden ⇒ „Kann gesendet sein – Pult neu laden.“ (nicht „nicht gesendet“), Text bleibt', (await textOf(noteOf(mail))) === 'Kann gesendet sein – Pult neu laden.' && (await area.inputValue()) === answer, await textOf(noteOf(mail)))
|
||||
|
||||
// 6. Senden: während die Anfrage läuft sind alle Knöpfe der Zeile gesperrt, ein zweiter Klick schickt nichts, danach Erfolgsmeldung oben
|
||||
await setPultFault(page, 'reply', null)
|
||||
await setDelay(page, 1500)
|
||||
const before = (await callsOf(page, 'reply')).length
|
||||
await send.click()
|
||||
const lockedNames = ['Ich kümmere mich', 'Als gesendet markieren', 'Als Mensch antworten', 'Verwerfen']
|
||||
const locked = []
|
||||
for (const name of lockedNames) locked.push(await mail.getByRole('button', { name, exact: true }).isDisabled())
|
||||
check('antworten: während die Anfrage läuft sind „Jetzt senden“ und alle Knöpfe der Zeile gesperrt', (await send.isDisabled()) && locked.every(Boolean), locked)
|
||||
await send.evaluate((el) => el.click()) // ein zweiter Klick auf den gesperrten Knopf darf nichts auslösen
|
||||
await shoot(page, 'antworten-pending.png')
|
||||
await finishedLine(page).first().waitFor()
|
||||
const done = await textOf(finishedLine(page).first())
|
||||
check('antworten: Erfolg ⇒ Meldung oben „…“ gesendet. (role=status), die Zeile ist weg', done === `„${T4.mail}“ gesendet.` && (await finishedLine(page).first().getAttribute('role')) === 'status' && (await mail.count()) === 0, done)
|
||||
const replyCalls = await callsOf(page, 'reply')
|
||||
const last = replyCalls[replyCalls.length - 1]
|
||||
check('antworten: Doppelklick während der Anfrage ergibt genau einen Aufruf; gesendet wird der getrimmte Text als { text }', replyCalls.length === before + 1 && last.id === 'p4-mail' && same(last.body, { text: answer.trim() }), { calls: replyCalls.length, before, last })
|
||||
check('antworten: die Antworten der Fehlerfälle gingen alle mit demselben getrimmten Text hinaus', replyCalls.slice(0, before).every((c) => c.body && c.body.text === answer.trim()), replyCalls.map((c) => c.body && c.body.text && c.body.text.length))
|
||||
await shoot(page, 'antworten-gesendet.png')
|
||||
await setDelay(page, 0)
|
||||
|
||||
// 7. Eintrag „unsicher“: Antwortfeld ohne Vorbelegung, Text überlebt Zuklappen und Filterwechsel
|
||||
check('antworten: „unsicher“-Zeile: Ich kümmere mich, Als Mensch antworten, Vorschau bauen, Erledigt, Verwerfen (kein Mail-Link, kein Entwurf)', same(await actionNames(uns), ['Ich kümmere mich', 'Als Mensch antworten', 'Vorschau bauen', 'Erledigt', 'Verwerfen']) && (await uns.locator('textarea').count()) === 0, await actionNames(uns))
|
||||
const unsOpener = uns.getByRole('button', { name: 'Als Mensch antworten', exact: true })
|
||||
await unsOpener.click()
|
||||
const unsPanel = uns.locator('.wk-pult-reply')
|
||||
await unsPanel.waitFor()
|
||||
const unsArea = unsPanel.locator('textarea')
|
||||
check('antworten: „unsicher“: Antwortfeld ist leer (kein Entwurf), Zähler „0 / 8000“', (await unsArea.inputValue()) === '' && (await textOf(unsPanel.locator('.wk-pult-count'))) === '0 / 8000')
|
||||
await unsArea.fill('Zwischenstand der Antwort')
|
||||
await unsOpener.click()
|
||||
check('antworten: zweiter Klick auf „Als Mensch antworten“ klappt das Feld zu (aria-expanded=false)', (await uns.locator('.wk-pult-reply').count()) === 0 && (await unsOpener.getAttribute('aria-expanded')) === 'false')
|
||||
await unsOpener.click()
|
||||
check('antworten: wieder aufgeklappt steht der Zwischenstand noch im Feld', (await uns.locator('.wk-pult-reply textarea').inputValue()) === 'Zwischenstand der Antwort')
|
||||
await chooseFilter4(page, 'Veraltet')
|
||||
await page.locator('.empty').waitFor()
|
||||
await chooseFilter4(page, 'Wartet')
|
||||
await waitTitles(page, [T4.frei, T4.falsch, T4.uns, T4.koll])
|
||||
await unsOpener.click()
|
||||
check('antworten: der Text überlebt einen Wechsel des Filters', (await uns.locator('.wk-pult-reply textarea').inputValue()) === 'Zwischenstand der Antwort')
|
||||
await shoot(page, 'antworten-unsicher.png')
|
||||
await setPultFault(page, 'reply', { status: 400, message: NEED_TEXT })
|
||||
await uns.locator('.wk-pult-reply').getByRole('button', { name: 'Jetzt senden', exact: true }).click()
|
||||
await uns.locator('.msg.err').waitFor()
|
||||
check('antworten: 400 vom Dienst („Bitte einen Text eingeben.“) erscheint in der Zeile, die Zeile bleibt', (await textOf(uns.locator('.msg.err'))) === NEED_TEXT && (await uns.count()) === 1)
|
||||
await setPultFault(page, 'reply', null)
|
||||
check('antworten: kein Telefon im Bild (weder Wort noch Nummer)', await phoneFree(page))
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario L: Vorschau freigeben (Desktop) ----------
|
||||
|
||||
async function vorschau(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'vorschau', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await openP4(page, base)
|
||||
const frei = row4(page, T4.frei)
|
||||
const falsch = row4(page, T4.falsch)
|
||||
await topOfPage(page)
|
||||
await frei.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'vorschau-wartet.png')
|
||||
|
||||
// 1. Zeile und Link
|
||||
check('vorschau: Art-Badge „Vorschau freigeben“, Knöpfe Ich kümmere mich, Vorschau ansehen, Freigeben & senden, Verwerfen (kein Erledigt, kein Als Mensch antworten)',
|
||||
(await textOf(frei.locator('.wk-pult-head .badge').first())) === 'Vorschau freigeben' && same(await actionNames(frei), ['Ich kümmere mich', 'Vorschau ansehen', 'Freigeben & senden', 'Verwerfen']), await actionNames(frei))
|
||||
const link = frei.getByRole('link', { name: `Vorschau ansehen: ${T4.frei}`, exact: true })
|
||||
check('vorschau: „Vorschau ansehen“ hat genau die https-Adresse der Vorschau', (await link.getAttribute('href')) === PREVIEW_URL, await link.getAttribute('href'))
|
||||
check('vorschau: „Vorschau ansehen“ öffnet im neuen Tab (target=_blank, rel=noopener noreferrer)', (await link.getAttribute('target')) === '_blank' && (await link.getAttribute('rel')) === 'noopener noreferrer')
|
||||
check('vorschau: das sichtbare Wort „Vorschau ansehen“ steht im Namen vorn', (await textOf(link)) === 'Vorschau ansehen' && (await link.getAttribute('aria-label')).startsWith('Vorschau ansehen'))
|
||||
check('vorschau: kein Link „Anzeige öffnen“ in der Zeile (die Adresse ist die Vorschau, keine Anzeige)', (await frei.getByRole('link', { name: /Anzeige öffnen/ }).count()) === 0)
|
||||
check('vorschau: Hinweis „Verwerfen“ sendet die Vorschau nicht.', (await textOf(frei.locator('.wk-pult-actions'))).includes('„Verwerfen“ sendet die Vorschau nicht.'))
|
||||
|
||||
// 2. Vorschau-Adresse mit http statt https: kein Link, Hinweis
|
||||
const fakeLinks = await falsch.locator('a').evaluateAll((els) => els.map((el) => el.getAttribute('href')))
|
||||
check('vorschau: Vorschau mit http-Adresse wird kein Link (Hinweis „Keine gültige Vorschau-Adresse hinterlegt.“)', !fakeLinks.some((href) => /entwurf\.project\.webklar\.com/.test(href || '')) && (await textOf(falsch.locator('.wk-pult-actions'))).includes(NO_PREVIEW_URL), fakeLinks)
|
||||
const falschApprove = falsch.getByRole('button', { name: 'Freigeben & senden', exact: true })
|
||||
metrics.freigabeOhneVorschauLink = { knopfDa: (await falschApprove.count()) === 1, knopfFrei: (await falschApprove.count()) === 1 && (await falschApprove.isEnabled()) }
|
||||
await falsch.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'vorschau-falsche-adresse.png')
|
||||
|
||||
// 3. Erster Klick fragt, nach 4 s verfällt die Frage
|
||||
const approveBtn = frei.getByRole('button', { name: /^(Freigeben & senden|Wirklich freigeben\?)$/ })
|
||||
await frei.scrollIntoViewIfNeeded()
|
||||
await approveBtn.click()
|
||||
check('vorschau: erster Klick fragt „Wirklich freigeben?“, noch keine Aktion am Dienst', (await textOf(approveBtn)) === 'Wirklich freigeben?' && (await callsOf(page, 'approve-preview')).length === 0, await textOf(approveBtn))
|
||||
await shoot(page, 'vorschau-frage.png')
|
||||
await sleep(4400)
|
||||
check('vorschau: nach 4 s verfällt die Rückfrage (wieder „Freigeben & senden“), noch immer keine Aktion', (await textOf(approveBtn)) === 'Freigeben & senden' && (await callsOf(page, 'approve-preview')).length === 0, await textOf(approveBtn))
|
||||
|
||||
// 4. Fehler vom Dienst (409): Satz in der Zeile, Zeile bleibt, Frage ist wieder zurückgesetzt
|
||||
await setPultFault(page, 'approve-preview', { status: 409, message: GONE })
|
||||
await approveBtn.click()
|
||||
await approveBtn.click()
|
||||
await frei.locator('.msg.err').waitFor()
|
||||
check('vorschau: 409 ⇒ „Dieser Kontakt ist nicht mehr aktiv.“ in der Zeile (rot, role=alert), Zeile bleibt', (await textOf(frei.locator('.msg.err'))) === GONE && (await frei.locator('.msg.err').getAttribute('role')) === 'alert' && (await frei.count()) === 1, await textOf(frei.locator('.msg.err')))
|
||||
check('vorschau: nach dem Fehler fragt der Knopf wieder von vorn („Freigeben & senden“, frei)', (await textOf(approveBtn)) === 'Freigeben & senden' && (await approveBtn.isEnabled()))
|
||||
await shoot(page, 'vorschau-409.png')
|
||||
await setPultFault(page, 'approve-preview', { status: 409, message: CONFLICT })
|
||||
await approveBtn.click()
|
||||
await approveBtn.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), CONFLICT)
|
||||
check('vorschau: zweiter 409 („Wird schon von einem Kollegen bearbeitet.“) ersetzt die Meldung', (await frei.locator('.msg').count()) === 1 && (await textOf(frei.locator('.msg'))) === CONFLICT)
|
||||
|
||||
await setPultFault(page, 'approve-preview', { status: 0, message: 'Verbindung weg' })
|
||||
await approveBtn.click()
|
||||
await approveBtn.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), 'Kann gesendet sein – Pult neu laden.')
|
||||
check('vorschau: Verbindungsabbruch bei der Freigabe ⇒ „Kann gesendet sein – Pult neu laden.“, die Zeile bleibt', (await textOf(frei.locator('.msg'))) === 'Kann gesendet sein – Pult neu laden.' && (await frei.count()) === 1)
|
||||
|
||||
// 5. Erfolg: zweiter Klick binnen 4 s sendet, währenddessen sind die Knöpfe gesperrt
|
||||
await setPultFault(page, 'approve-preview', null)
|
||||
await setDelay(page, 1500)
|
||||
const before = (await callsOf(page, 'approve-preview')).length
|
||||
await approveBtn.click()
|
||||
await approveBtn.click()
|
||||
const lockedNames = ['Ich kümmere mich', 'Freigeben & senden', 'Verwerfen']
|
||||
const locked = []
|
||||
for (const name of lockedNames) locked.push(await frei.getByRole('button', { name, exact: true }).isDisabled())
|
||||
check('vorschau: während die Freigabe läuft sind „Freigeben & senden“, „Ich kümmere mich“ und „Verwerfen“ gesperrt', locked.every(Boolean), locked)
|
||||
await approveBtn.evaluate((el) => el.click()) // weiterer Klick auf den gesperrten Knopf
|
||||
await shoot(page, 'vorschau-pending.png')
|
||||
await finishedLine(page).first().waitFor()
|
||||
const done = await textOf(finishedLine(page).first())
|
||||
check('vorschau: Erfolg ⇒ Meldung oben „…“ freigegeben. Max sendet den Link im nächsten Sendefenster. (role=status), Zeile weg', done === `„${T4.frei}“ freigegeben. Max sendet den Link im nächsten Sendefenster.` && (await frei.count()) === 0 && (await finishedLine(page).first().getAttribute('role')) === 'status', done)
|
||||
const calls = await callsOf(page, 'approve-preview', 'p4-freigabe')
|
||||
check('vorschau: genau ein erfolgreicher Aufruf ohne Body (drei Fehlversuche davor, der Doppelklick im Warten zählt nicht)', calls.length === before + 1 && calls.every((c) => c.body === undefined), calls)
|
||||
await shoot(page, 'vorschau-freigegeben.png')
|
||||
await setDelay(page, 0)
|
||||
check('vorschau: die andere Freigabe (falsche Adresse) und die übrigen Zeilen bleiben unberührt', same(await rowTitles(page), [T4.mail, T4.falsch, T4.uns, T4.koll]), await rowTitles(page))
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario M: Antwort des Kunden einfügen (Desktop) ----------
|
||||
|
||||
async function einfuegen(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'einfuegen', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await openP4Done(page, base)
|
||||
const plat = row4(page, T4.plat)
|
||||
const ang = row4(page, T4.ang)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'einfuegen-erledigt.png')
|
||||
|
||||
// 1. Zwei Zeilen desselben Interessenten, beide gesendet, beide bieten das Einfügen
|
||||
check('einfuegen: Erledigt zeigt zwei gesendete Plattform-Antworten desselben Interessenten (Standard-Platz und „angebot“)', same(await rowTitles(page), [T4.ang, T4.plat]), await rowTitles(page))
|
||||
check('einfuegen: beide Zeilen sind „Gesendet von Kenso“ und bieten genau „Antwort des Kunden einfügen“', same(await actionNames(plat), ['Antwort des Kunden einfügen']) && same(await actionNames(ang), ['Antwort des Kunden einfügen']) && (await textOf(plat.locator('.wk-pult-meta'))).startsWith('Gesendet von Kenso') && (await textOf(ang.locator('.wk-pult-meta'))).startsWith('Gesendet von Kenso'), { plat: await actionNames(plat), ang: await actionNames(ang) })
|
||||
const platOpen = plat.getByRole('button', { name: 'Antwort des Kunden einfügen', exact: true })
|
||||
const angOpen = ang.getByRole('button', { name: 'Antwort des Kunden einfügen', exact: true })
|
||||
|
||||
// 2. Aufklappen: Hinweis, leeres Feld, Zähler, Knopf
|
||||
check('einfuegen: vor dem Öffnen kein Feld, Knopf mit aria-expanded=false', (await plat.locator('.wk-pult-paste').count()) === 0 && (await platOpen.getAttribute('aria-expanded')) === 'false')
|
||||
await platOpen.click()
|
||||
const platPanel = plat.locator('.wk-pult-paste')
|
||||
await platPanel.waitFor()
|
||||
const platArea = platPanel.locator('textarea')
|
||||
const platSend = platPanel.getByRole('button', { name: 'Einfügen', exact: true })
|
||||
check('einfuegen: Hinweistext „Kopiere die Antwort aus der Plattform hierher. …“', (await textOf(platPanel.locator('.wk-hint'))) === PASTE_HINT, await textOf(platPanel.locator('.wk-hint')))
|
||||
check('einfuegen: Textfeld ist leer, heißt „Antwort des Kunden: …“, Zähler „0 / 8000“, Knopf „Einfügen“', (await platArea.inputValue()) === '' && (await platArea.getAttribute('aria-label')) === `Antwort des Kunden: ${T4.plat}` && (await textOf(platPanel.locator('.wk-pult-count'))) === '0 / 8000' && (await platSend.count()) === 1 && (await platOpen.getAttribute('aria-expanded')) === 'true')
|
||||
check('einfuegen: erste Zeile offen, zweite Zeile bleibt zu', (await ang.locator('.wk-pult-paste').count()) === 0)
|
||||
|
||||
// 3. Leerer Text: Fehler in der Oberfläche, kein Aufruf
|
||||
await platSend.click()
|
||||
await plat.locator('.msg.err').waitFor()
|
||||
check('einfuegen: leerer Text ⇒ „Bitte einen Text eingeben.“ (rot, role=alert), kein Aufruf am Dienst', (await textOf(plat.locator('.msg.err'))) === NEED_TEXT && (await plat.locator('.msg.err').getAttribute('role')) === 'alert' && (await callsOf(page, 'paste')).length === 0, { text: await textOf(plat.locator('.msg.err')), calls: (await callsOf(page, 'paste')).length })
|
||||
await shoot(page, 'einfuegen-leer-fehler.png')
|
||||
|
||||
// 4. Beide Zeilen unabhängig: eigene Felder, eigene Meldungen
|
||||
await platArea.fill('Antwort Kunde A')
|
||||
await angOpen.click()
|
||||
const angPanel = ang.locator('.wk-pult-paste')
|
||||
await angPanel.waitFor()
|
||||
const angArea = angPanel.locator('textarea')
|
||||
const angSend = angPanel.getByRole('button', { name: 'Einfügen', exact: true })
|
||||
check('einfuegen: zweite Zeile öffnet ein eigenes, leeres Feld (kein Text der ersten)', (await angArea.inputValue()) === '' && (await platArea.inputValue()) === 'Antwort Kunde A')
|
||||
await angArea.fill('Antwort Kunde B')
|
||||
check('einfuegen: beide Felder gleichzeitig offen, jedes mit eigenem Text und eigenem Zähler', (await platArea.inputValue()) === 'Antwort Kunde A' && (await angArea.inputValue()) === 'Antwort Kunde B' && (await textOf(platPanel.locator('.wk-pult-count'))) === '15 / 8000' && (await textOf(angPanel.locator('.wk-pult-count'))) === '15 / 8000')
|
||||
await shoot(page, 'einfuegen-zwei-offen.png')
|
||||
check('einfuegen: die Fehlermeldung der ersten Zeile steht nur dort', (await ang.locator('.msg').count()) === 0)
|
||||
|
||||
// 5. Fehler vom Dienst: 413, 400 und 409 mit deutschem Satz; Text und Feld bleiben
|
||||
await setPultFault(page, 'paste', { status: 413, message: TOO_BIG })
|
||||
await platSend.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), TOO_BIG)
|
||||
check('einfuegen: 413 ⇒ „Anfrage zu groß.“ (rot, role=alert) in der ersten Zeile; Feld und Text bleiben, Knopf frei', (await textOf(plat.locator('.msg.err'))) === TOO_BIG && (await platArea.inputValue()) === 'Antwort Kunde A' && (await platSend.isEnabled()), await textOf(plat.locator('.msg.err')))
|
||||
await shoot(page, 'einfuegen-413.png')
|
||||
await setPultFault(page, 'paste', { status: 400, message: NEED_TEXT })
|
||||
await platSend.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), NEED_TEXT)
|
||||
check('einfuegen: 400 vom Dienst ⇒ „Bitte einen Text eingeben.“, Meldung ersetzt die vorige', (await plat.locator('.msg').count()) === 1 && (await textOf(plat.locator('.msg'))) === NEED_TEXT)
|
||||
await shoot(page, 'einfuegen-400.png')
|
||||
await setPultFault(page, 'paste', { status: 409, message: GONE })
|
||||
await platSend.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), GONE)
|
||||
check('einfuegen: 409 ⇒ deutscher Satz des Dienstes in der Zeile', (await textOf(plat.locator('.msg'))) === GONE)
|
||||
check('einfuegen: die zweite Zeile zeigt von all dem nichts (keine Meldung, Text bleibt)', (await ang.locator('.msg').count()) === 0 && (await angArea.inputValue()) === 'Antwort Kunde B')
|
||||
await setPultFault(page, 'paste', { status: 0, message: 'Verbindung weg' })
|
||||
await platSend.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), 'Der Hustler-Dienst ist gerade nicht erreichbar.')
|
||||
check('einfuegen: Verbindungsabbruch ⇒ „Der Hustler-Dienst ist gerade nicht erreichbar.“, Text bleibt im Feld', (await textOf(plat.locator('.msg'))) === 'Der Hustler-Dienst ist gerade nicht erreichbar.' && (await platArea.inputValue()) === 'Antwort Kunde A')
|
||||
await setPultFault(page, 'paste', null)
|
||||
|
||||
// 6. Erfolg der ersten Zeile: gesperrt beim Warten, Meldung in der Zeile, Feld leer und zu, Zeile bleibt, zweite Zeile unberührt.
|
||||
// Die Wartezeit liegt unter der Zeitgrenze des Prüfstands (1,5 s, entry.jsx): „paste“ hat anders als reply, approve-preview und start-build
|
||||
// keine lange Frist (SLOW_PULT_ACTIONS), eine längere Wartezeit endete hier als „nicht erreichbar“.
|
||||
await setDelay(page, 900)
|
||||
await platSend.click()
|
||||
check('einfuegen: während die Anfrage läuft sind „Einfügen“ und „Antwort des Kunden einfügen“ der Zeile gesperrt', (await platSend.isDisabled()) && (await platOpen.isDisabled()))
|
||||
check('einfuegen: die zweite Zeile bleibt währenddessen bedienbar', (await angSend.isEnabled()) && (await angOpen.isEnabled()))
|
||||
await platSend.evaluate((el) => el.click())
|
||||
await shoot(page, 'einfuegen-pending.png')
|
||||
await plat.locator('.msg.ok').waitFor()
|
||||
await setDelay(page, 0)
|
||||
check('einfuegen: Erfolg ⇒ „Eingefügt. Nora sortiert die Antwort ein.“ (grün, role=status) in der Zeile', (await textOf(plat.locator('.msg.ok'))) === PASTE_OK && (await plat.locator('.msg.ok').getAttribute('role')) === 'status', await textOf(plat.locator('.msg')))
|
||||
check('einfuegen: danach ist das Feld zu, die Zeile bleibt in der Liste, die zweite Zeile ist unberührt (Feld offen, Text, keine Meldung)', (await plat.locator('.wk-pult-paste').count()) === 0 && (await plat.count()) === 1 && (await ang.locator('.wk-pult-paste textarea').inputValue()) === 'Antwort Kunde B' && (await ang.locator('.msg').count()) === 0)
|
||||
const pasteCalls = await callsOf(page, 'paste')
|
||||
const okCall = pasteCalls[pasteCalls.length - 1]
|
||||
check('einfuegen: der Aufruf geht an den Eintrag der ersten Zeile mit { text: „Antwort Kunde A“ }; Doppelklick im Warten zählt nicht', okCall.id === 'p4-plat-gesendet' && same(okCall.body, { text: 'Antwort Kunde A' }) && pasteCalls.filter((c) => c.id === 'p4-plat-gesendet').length === 5 && pasteCalls.every((c) => c.id !== 'p4-plat-angebot'), pasteCalls)
|
||||
await shoot(page, 'einfuegen-erfolg.png')
|
||||
|
||||
// 7. Dieselbe Antwort noch einmal: „doppelt“
|
||||
await platOpen.click()
|
||||
const again = plat.locator('.wk-pult-paste textarea')
|
||||
await again.waitFor()
|
||||
check('einfuegen: wieder geöffnet ist das Feld leer (nach dem Erfolg geleert)', (await again.inputValue()) === '')
|
||||
await again.fill('Antwort Kunde A')
|
||||
await plat.locator('.wk-pult-paste').getByRole('button', { name: 'Einfügen', exact: true }).click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg')).some((el) => el.textContent === t), PASTE_TWICE)
|
||||
check('einfuegen: dieselbe Antwort ein zweites Mal ⇒ „Diese Antwort war schon eingefügt.“', (await textOf(plat.locator('.msg'))) === PASTE_TWICE, await textOf(plat.locator('.msg')))
|
||||
|
||||
// 8. Zweite Zeile sendet für sich
|
||||
await angSend.click()
|
||||
await ang.locator('.msg.ok').waitFor()
|
||||
const angCall = (await callsOf(page, 'paste', 'p4-plat-angebot')).pop()
|
||||
check('einfuegen: zweite Zeile schickt ihren eigenen Text an ihren eigenen Eintrag, ihre Meldung steht nur dort', (await textOf(ang.locator('.msg.ok'))) === PASTE_OK && angCall && same(angCall.body, { text: 'Antwort Kunde B' }) && (await textOf(plat.locator('.msg'))) === PASTE_TWICE)
|
||||
await shoot(page, 'einfuegen-beide-fertig.png')
|
||||
check('einfuegen: kein Telefon im Bild (weder Wort noch Nummer)', await phoneFree(page))
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario N: Vorschau bauen (Desktop) ----------
|
||||
|
||||
async function bauen(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'bauen', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await openP4(page, base)
|
||||
const uns = row4(page, T4.uns)
|
||||
const build = uns.getByRole('button', { name: /^(Vorschau bauen|Wirklich bauen\?)$/ })
|
||||
await uns.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'bauen-zeile.png')
|
||||
check('bauen: Zeile „unsicher“ bietet „Vorschau bauen“ (neben Ich kümmere mich, Als Mensch antworten, Erledigt, Verwerfen)', same(await actionNames(uns), ['Ich kümmere mich', 'Als Mensch antworten', 'Vorschau bauen', 'Erledigt', 'Verwerfen']), await actionNames(uns))
|
||||
check('bauen: die Kollegen-Zeile und die Mail-Zeile bieten kein „Vorschau bauen“', (await row4(page, T4.koll).getByRole('button', { name: 'Vorschau bauen' }).count()) === 0 && (await row4(page, T4.mail).getByRole('button', { name: 'Vorschau bauen' }).count()) === 0)
|
||||
|
||||
// 1. Fehler vom Dienst
|
||||
await setPultFault(page, 'start-build', { status: 409, message: 'Für diesen Interessenten gibt es schon eine Vorschau.' })
|
||||
await build.click()
|
||||
await uns.locator('.msg.err').waitFor()
|
||||
check('bauen: 409 ⇒ „Für diesen Interessenten gibt es schon eine Vorschau.“ in der Zeile (rot, role=alert), Zeile bleibt', (await textOf(uns.locator('.msg.err'))) === 'Für diesen Interessenten gibt es schon eine Vorschau.' && (await uns.locator('.msg.err').getAttribute('role')) === 'alert' && (await uns.count()) === 1, await textOf(uns.locator('.msg.err')))
|
||||
check('bauen: nach dem Fehler ist „Vorschau bauen“ wieder frei', await uns.getByRole('button', { name: 'Vorschau bauen', exact: true }).isEnabled())
|
||||
await shoot(page, 'bauen-409.png')
|
||||
await setPultFault(page, 'start-build', { status: 409, message: GONE })
|
||||
await build.click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row .msg.err')).some((el) => el.textContent === t), GONE)
|
||||
check('bauen: zweiter 409 („Dieser Kontakt ist nicht mehr aktiv.“) ersetzt die Meldung', (await uns.locator('.msg').count()) === 1 && (await textOf(uns.locator('.msg'))) === GONE)
|
||||
await setPultFault(page, 'start-build', null)
|
||||
|
||||
// 2. Fragt „Vorschau bauen“ nach? Festgestellt wird nur (metrics.bauenFragtNach): ruft der erste Klick den Dienst auf, gibt es keine Rückfrage
|
||||
// (anders als bei Freigeben und Verwerfen). Fragt der Knopf nach, bestätigt der zweite Klick, damit der Rest des Szenarios gleich läuft.
|
||||
await setDelay(page, 1500)
|
||||
const before = (await callsOf(page, 'start-build')).length
|
||||
await build.click()
|
||||
await sleep(150)
|
||||
const afterFirst = (await callsOf(page, 'start-build')).length
|
||||
metrics.bauenFragtNach = afterFirst === before
|
||||
metrics.bauenErsterKlickRuftDienst = afterFirst === before + 1
|
||||
if (afterFirst === before) await build.click()
|
||||
check('bauen: während der Bau angelegt wird sind alle Knöpfe der Zeile gesperrt', (await uns.getByRole('button', { name: 'Ich kümmere mich', exact: true }).isDisabled()) && (await uns.getByRole('button', { name: 'Als Mensch antworten', exact: true }).isDisabled()) && (await uns.getByRole('button', { name: 'Erledigt', exact: true }).isDisabled()) && (await uns.getByRole('button', { name: 'Verwerfen', exact: true }).isDisabled()) && (await uns.getByRole('button', { name: 'Vorschau bauen', exact: true }).isDisabled()))
|
||||
await shoot(page, 'bauen-pending.png')
|
||||
await finishedLine(page).first().waitFor()
|
||||
await setDelay(page, 0)
|
||||
check('bauen: Erfolg ⇒ Meldung oben „…“ wird gebaut. (role=status), die Zeile ist weg', (await textOf(finishedLine(page).first())) === `„${T4.uns}“ wird gebaut.` && (await finishedLine(page).first().getAttribute('role')) === 'status' && (await uns.count()) === 0, await textOf(finishedLine(page).first()))
|
||||
const calls = await callsOf(page, 'start-build', 'p4-unsicher')
|
||||
check('bauen: genau ein erfolgreicher Aufruf ohne Body (nach den zwei Fehlversuchen)', calls.length === before + 1 && calls.every((c) => c.body === undefined), calls)
|
||||
await shoot(page, 'bauen-gestartet.png')
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario O: Fokus der aufgeklappten Felder und nach den Aktionen (Desktop) ----------
|
||||
|
||||
async function fokus(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'fokus', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await openP4(page, base)
|
||||
const mail = row4(page, T4.mail)
|
||||
const noBody = (info) => info.tag !== null && info.tag !== 'BODY'
|
||||
|
||||
// Esc schließt das Feld; gibt es keinen Esc-Weg, zählt ein Knopf „Abbrechen“. Ergebnis, ob eines von beiden das Feld schließt und wo dann der Fokus ist.
|
||||
async function leaveByKeyboard(row, panelSelector, probe) {
|
||||
const detached = (ms) => row.locator(panelSelector).waitFor({ state: 'detached', timeout: ms }).then(() => true, () => false)
|
||||
await page.keyboard.press('Escape')
|
||||
let how = 'esc'
|
||||
let closed = await detached(700)
|
||||
if (!closed) {
|
||||
const cancel = row.locator(panelSelector).getByRole('button', { name: 'Abbrechen', exact: true })
|
||||
if ((await cancel.count()) > 0) {
|
||||
how = 'abbrechen'
|
||||
await cancel.click()
|
||||
closed = await detached(700)
|
||||
} else {
|
||||
how = 'weder Esc noch Abbrechen'
|
||||
}
|
||||
}
|
||||
return { how, closed, focus: await focusInfo(page), onOpener: (await focusInfo(page)).probe === probe }
|
||||
}
|
||||
|
||||
// 1. „Als Mensch antworten“
|
||||
const replyOpener = mail.getByRole('button', { name: 'Als Mensch antworten', exact: true })
|
||||
await replyOpener.evaluate((el) => el.setAttribute('data-probe', 'reply-opener'))
|
||||
await replyOpener.click()
|
||||
await mail.locator('.wk-pult-reply').waitFor()
|
||||
const f1 = await focusInfo(page)
|
||||
check('fokus: „Als Mensch antworten“ öffnen ⇒ der Fokus liegt im Textfeld des Antwortfelds', f1.tag === 'TEXTAREA' && f1.inReply, f1)
|
||||
await shoot(page, 'fokus-antwort-offen.png')
|
||||
const l1 = await leaveByKeyboard(mail, '.wk-pult-reply', 'reply-opener')
|
||||
check('fokus: Esc oder „Abbrechen“ schließt das Antwortfeld und gibt den Fokus an „Als Mensch antworten“ zurück', l1.closed && l1.onOpener, l1)
|
||||
if (!l1.closed) {
|
||||
await replyOpener.click() // Rückweg über den Knopf selbst, damit der Rest des Szenarios weiterläuft
|
||||
await mail.locator('.wk-pult-reply').waitFor({ state: 'detached' })
|
||||
}
|
||||
const f1b = await focusInfo(page)
|
||||
check('fokus: zweiter Klick auf „Als Mensch antworten“ schließt das Feld, der Fokus bleibt auf dem Knopf (nicht auf <body>)', (await mail.locator('.wk-pult-reply').count()) === 0 && f1b.probe === 'reply-opener', f1b)
|
||||
|
||||
// 2. „Details“ (Dialog)
|
||||
const details = mail.getByRole('button', { name: `Details zu ${T4.mail}`, exact: true })
|
||||
await details.evaluate((el) => el.setAttribute('data-probe', 'details-opener'))
|
||||
await details.click()
|
||||
await page.getByRole('dialog', { name: 'SV Namborn', exact: true }).waitFor()
|
||||
await sleep(500)
|
||||
check('fokus: „Details“ öffnen ⇒ der Fokus liegt im Dialog', (await focusInfo(page)).inDialog, await focusInfo(page))
|
||||
await page.keyboard.press('Escape')
|
||||
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
||||
check('fokus: Esc schließt „Details“, der Fokus kehrt auf „Details“ zurück', (await focusInfo(page)).probe === 'details-opener', await focusInfo(page))
|
||||
|
||||
// 3. Fokus nach Fehler und nach Erfolg
|
||||
await replyOpener.click()
|
||||
const area = mail.locator('.wk-pult-reply textarea')
|
||||
await area.waitFor()
|
||||
await setPultFault(page, 'reply', { status: 409, message: CONFLICT })
|
||||
await mail.locator('.wk-pult-reply').getByRole('button', { name: 'Jetzt senden', exact: true }).click()
|
||||
await mail.locator('.msg.err').waitFor()
|
||||
await sleep(200)
|
||||
const fErr = await focusInfo(page)
|
||||
check('fokus: nach einem Fehler (409) beim Senden steht der Fokus nicht auf <body> (Knopf oder Feld)', noBody(fErr), fErr)
|
||||
await setPultFault(page, 'reply', null)
|
||||
await mail.locator('.wk-pult-reply').getByRole('button', { name: 'Jetzt senden', exact: true }).click()
|
||||
await finishedLine(page).first().waitFor()
|
||||
await sleep(200)
|
||||
const fReply = await focusInfo(page)
|
||||
check('fokus: nach dem erfolgreichen Senden einer Antwort (Zeile verschwindet) steht der Fokus nicht auf <body>', noBody(fReply), fReply)
|
||||
|
||||
const frei = row4(page, T4.frei)
|
||||
const approveBtn = frei.getByRole('button', { name: /^(Freigeben & senden|Wirklich freigeben\?)$/ })
|
||||
await approveBtn.click()
|
||||
await approveBtn.click()
|
||||
await page.waitForFunction((t) => document.querySelector('.wk-pult-filters ~ .msg') && document.querySelector('.wk-pult-filters ~ .msg').textContent.includes(t), 'freigegeben')
|
||||
await sleep(200)
|
||||
const fApprove = await focusInfo(page)
|
||||
check('fokus: nach „Freigeben & senden“ (Zeile verschwindet) steht der Fokus nicht auf <body>', noBody(fApprove), fApprove)
|
||||
|
||||
const uns = row4(page, T4.uns)
|
||||
await uns.getByRole('button', { name: 'Vorschau bauen', exact: true }).click()
|
||||
await page.waitForFunction((t) => document.querySelector('.wk-pult-filters ~ .msg') && document.querySelector('.wk-pult-filters ~ .msg').textContent.includes(t), 'wird gebaut')
|
||||
await sleep(200)
|
||||
const fBuild = await focusInfo(page)
|
||||
check('fokus: nach „Vorschau bauen“ (Zeile verschwindet) steht der Fokus nicht auf <body>', noBody(fBuild), fBuild)
|
||||
|
||||
// 4. „Antwort des Kunden einfügen“ (Reiter Erledigt)
|
||||
await chooseFilter4(page, 'Erledigt')
|
||||
await waitTitles(page, [T4.mail, T4.frei, T4.uns, T4.ang, T4.plat]) // Mail, Freigabe und unsicher sind durch die Aktionen davor hierher gewandert
|
||||
const plat = row4(page, T4.plat)
|
||||
const pasteOpener = plat.getByRole('button', { name: 'Antwort des Kunden einfügen', exact: true })
|
||||
await pasteOpener.evaluate((el) => el.setAttribute('data-probe', 'paste-opener'))
|
||||
await pasteOpener.click()
|
||||
await plat.locator('.wk-pult-paste').waitFor()
|
||||
const f4 = await focusInfo(page)
|
||||
check('fokus: „Antwort des Kunden einfügen“ öffnen ⇒ der Fokus liegt im Textfeld', f4.tag === 'TEXTAREA' && f4.inPaste, f4)
|
||||
await shoot(page, 'fokus-einfuegen-offen.png')
|
||||
const l4 = await leaveByKeyboard(plat, '.wk-pult-paste', 'paste-opener')
|
||||
check('fokus: Esc oder „Abbrechen“ schließt das Einfüge-Feld und gibt den Fokus an „Antwort des Kunden einfügen“ zurück', l4.closed && l4.onOpener, l4)
|
||||
if (!l4.closed) {
|
||||
await pasteOpener.click()
|
||||
await plat.locator('.wk-pult-paste').waitFor({ state: 'detached' })
|
||||
}
|
||||
await pasteOpener.click()
|
||||
await plat.locator('.wk-pult-paste textarea').fill('Danke, das passt.')
|
||||
await plat.locator('.wk-pult-paste').getByRole('button', { name: 'Einfügen', exact: true }).click()
|
||||
await plat.locator('.msg.ok').waitFor()
|
||||
await sleep(200)
|
||||
const fPaste = await focusInfo(page)
|
||||
check('fokus: nach dem erfolgreichen Einfügen (Feld klappt zu) steht der Fokus nicht auf <body>', noBody(fPaste), fPaste)
|
||||
|
||||
// Zum Vergleich (nur festgehalten, kein Prüfpunkt): „Ich kümmere mich“ aus P3 behält die Zeile, der Knopf ist beim Warten gesperrt
|
||||
await chooseFilter4(page, 'Wartet')
|
||||
await waitTitles(page, [T4.falsch, T4.koll])
|
||||
await row4(page, T4.falsch).getByRole('button', { name: 'Ich kümmere mich', exact: true }).click()
|
||||
await page.waitForFunction((t) => Array.from(document.querySelectorAll('li.wk-pult-row')).some((el) => el.textContent.includes(t) && el.querySelector('.wk-pult-meta').textContent.includes('übernommen von Kenso')), T4.falsch)
|
||||
await sleep(200)
|
||||
metrics.fokusNachIchKuemmereMich = (await focusInfo(page)).tag
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario P: Handy 390 x 844 für alle Felder und Zustände ----------
|
||||
|
||||
async function handyP4(browser, base) {
|
||||
const { context, page } = await newPultPage(browser, 'handy-p4', { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true })
|
||||
await openP4(page, base)
|
||||
await sleep(300)
|
||||
const mail = row4(page, T4.mail)
|
||||
const frei = row4(page, T4.frei)
|
||||
const falsch = row4(page, T4.falsch)
|
||||
const uns = row4(page, T4.uns)
|
||||
const noScroll = async (label, detail) => {
|
||||
const w = await widthOf(page)
|
||||
check(`handy-p4: ${label}: kein seitliches Scrollen (scrollWidth <= innerWidth)`, w.scroll <= w.inner, { ...w, ...(detail || {}) })
|
||||
}
|
||||
const ACTIONS = '.wk-pult-actions > a, .wk-pult-actions > button'
|
||||
|
||||
// 1. Liste
|
||||
await noScroll('Liste Wartet')
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-p4-wartet.png')
|
||||
await shoot(page, 'handy-p4-wartet-ganz.png', { fullPage: true })
|
||||
const reach1 = await reachOf(page, ACTIONS)
|
||||
check('handy-p4: alle Aktionsknöpfe und Links der Liste sind erreichbar (im Fenster, nicht verdeckt, mindestens 40 px hoch)', reach1.length >= 15 && badReach(reach1).length === 0, { count: reach1.length, bad: badReach(reach1) })
|
||||
|
||||
// 2. „Als Mensch antworten“ mit sehr langem Wort, Fehlermeldung
|
||||
await mail.scrollIntoViewIfNeeded()
|
||||
await mail.getByRole('button', { name: 'Als Mensch antworten', exact: true }).click()
|
||||
const panel = mail.locator('.wk-pult-reply')
|
||||
await panel.waitFor()
|
||||
await sleep(200)
|
||||
await noScroll('Antwortfeld offen')
|
||||
const reach2 = await reachOf(page, '.wk-pult-reply textarea, .wk-pult-reply .wk-pult-panel-foot .btn')
|
||||
const area = panel.locator('textarea')
|
||||
const areaBox = await area.evaluate((el) => { const r = el.getBoundingClientRect(); return { left: Math.round(r.left), right: Math.round(r.right), height: Math.round(r.height) } })
|
||||
check('handy-p4: Antwortfeld: Textfeld und „Jetzt senden“ im Fenster, nicht verdeckt, Knopf mindestens 40 px hoch, Feld mindestens 100 px hoch', reach2.length === 2 && badReach(reach2).length === 0 && areaBox.left >= 0 && areaBox.right <= 390 && areaBox.height >= 100, { reach2, areaBox })
|
||||
await panel.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'handy-p4-antwort-offen.png')
|
||||
await area.fill('x'.repeat(400) + ' https://www.sv-namborn.example/' + 'lang/'.repeat(30))
|
||||
await noScroll('Antwortfeld mit sehr langem Text')
|
||||
await setPultFault(page, 'reply', { status: 409, message: 'Wird schon von einem Kollegen bearbeitet.' })
|
||||
await panel.getByRole('button', { name: 'Jetzt senden', exact: true }).click()
|
||||
await mail.locator('.msg.err').waitFor()
|
||||
await setPultFault(page, 'reply', null)
|
||||
await noScroll('Antwortfeld mit Fehlermeldung')
|
||||
await mail.locator('.msg.err').scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'handy-p4-antwort-409.png')
|
||||
|
||||
// 3. Vorschau-Freigabe: Frage, Link, falsche Adresse
|
||||
await frei.scrollIntoViewIfNeeded()
|
||||
const approveBtn = frei.getByRole('button', { name: /^(Freigeben & senden|Wirklich freigeben\?)$/ })
|
||||
await approveBtn.click()
|
||||
check('handy-p4: Freigabe-Zeile fragt „Wirklich freigeben?“', (await textOf(approveBtn)) === 'Wirklich freigeben?')
|
||||
await noScroll('Freigabe-Zeile mit Rückfrage')
|
||||
await shoot(page, 'handy-p4-freigabe-frage.png')
|
||||
await falsch.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'handy-p4-freigabe-falsche-adresse.png')
|
||||
|
||||
// 4. „unsicher“: Antwortfeld leer, „Vorschau bauen“
|
||||
await uns.scrollIntoViewIfNeeded()
|
||||
await uns.getByRole('button', { name: 'Als Mensch antworten', exact: true }).click()
|
||||
await uns.locator('.wk-pult-reply').waitFor()
|
||||
await noScroll('Zeile unsicher mit Antwortfeld')
|
||||
const reach4 = await reachOf(page, 'li.wk-pult-row .wk-pult-reply .btn, li.wk-pult-row .wk-pult-actions > button')
|
||||
check('handy-p4: alle Knöpfe von Antwortfeld und Aktionen sind erreichbar (nicht verdeckt, im Fenster, mindestens 40 px hoch)', reach4.length >= 10 && badReach(reach4).length === 0, { count: reach4.length, bad: badReach(reach4) })
|
||||
await uns.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'handy-p4-unsicher.png')
|
||||
|
||||
// 5. „Details“ als Panel
|
||||
const details = uns.getByRole('button', { name: `Details zu ${T4.uns}`, exact: true })
|
||||
await details.scrollIntoViewIfNeeded()
|
||||
await details.click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Gasthaus Sonne', exact: true })
|
||||
await dialog.waitFor()
|
||||
await dialog.locator('.wk-prospect').waitFor()
|
||||
await sleep(700)
|
||||
const box = await dialog.evaluate((el) => { const r = el.getBoundingClientRect(); return { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top), bottom: Math.round(r.bottom), inner: window.innerHeight } })
|
||||
check('handy-p4: „Details“-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)
|
||||
await noScroll('Details-Panel offen')
|
||||
await shoot(page, 'handy-p4-details.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), height: Math.round(r.height), inner: window.innerHeight, covered: !(hit && (hit === el || el.contains(hit))) } })
|
||||
check('handy-p4: „Schließen“ im Details-Panel ist erreichbar (im Fenster, nicht verdeckt, mindestens 40 px hoch)', closeReach.left >= 0 && closeReach.right <= 390 && closeReach.bottom <= closeReach.inner && !closeReach.covered && closeReach.height >= 40, closeReach)
|
||||
await closeBtn.click()
|
||||
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
||||
|
||||
// 6. Erledigt: „Antwort des Kunden einfügen“ in beiden Zeilen, 413
|
||||
await chooseFilter4(page, 'Erledigt')
|
||||
await waitTitles(page, [T4.ang, T4.plat])
|
||||
const plat = row4(page, T4.plat)
|
||||
const ang = row4(page, T4.ang)
|
||||
await noScroll('Liste Erledigt')
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'handy-p4-erledigt.png')
|
||||
await plat.getByRole('button', { name: 'Antwort des Kunden einfügen', exact: true }).click()
|
||||
await ang.getByRole('button', { name: 'Antwort des Kunden einfügen', exact: true }).click()
|
||||
await plat.locator('.wk-pult-paste').waitFor()
|
||||
await ang.locator('.wk-pult-paste').waitFor()
|
||||
await plat.locator('.wk-pult-paste textarea').fill('Antwort Kunde A')
|
||||
await setPultFault(page, 'paste', { status: 413, message: TOO_BIG })
|
||||
await plat.locator('.wk-pult-paste').getByRole('button', { name: 'Einfügen', exact: true }).click()
|
||||
await plat.locator('.msg.err').waitFor()
|
||||
await setPultFault(page, 'paste', null)
|
||||
await noScroll('beide Einfüge-Felder offen, 413 sichtbar')
|
||||
const reach6 = await reachOf(page, '.wk-pult-paste textarea, .wk-pult-paste .wk-pult-panel-foot .btn, li.wk-pult-row .wk-pult-actions > button')
|
||||
check('handy-p4: Einfüge-Felder, „Einfügen“ und „Antwort des Kunden einfügen“ sind erreichbar (im Fenster, nicht verdeckt, Knöpfe mindestens 40 px hoch)', reach6.length === 6 && badReach(reach6).length === 0, { count: reach6.length, bad: badReach(reach6) })
|
||||
await plat.scrollIntoViewIfNeeded()
|
||||
await shoot(page, 'handy-p4-einfuegen-413.png')
|
||||
await shoot(page, 'handy-p4-einfuegen-ganz.png', { fullPage: true })
|
||||
check('handy-p4: kein Telefon im Bild (weder Wort noch Nummer)', await phoneFree(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],
|
||||
['antworten', antworten], ['vorschau', vorschau], ['einfuegen', einfuegen], ['bauen', bauen], ['fokus', fokus], ['handy-p4', handyP4],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
@@ -999,6 +1689,7 @@ const SCENARIOS = [
|
||||
await run(browser, base)
|
||||
} catch (e) {
|
||||
problems.push('Abbruch in Szenario ' + name + ': ' + (e && e.stack ? e.stack : String(e)))
|
||||
if (lastPage) await shoot(lastPage, 'abbruch-' + name + '.png', { fullPage: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user