Workers P2: Prüfstand mit Skript-API und Playwright-Abnahme
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b78379b9b
commit
d67f8cc471
6 files changed
+896
No files matched your search
@@ -0,0 +1,381 @@
|
||||
// Manuelle Abnahme der Hustler-Seite im echten Browser (Chromium, headless shell) gegen den Prüfstand.
|
||||
// Aufruf (immer unter Speicherdeckel, serielle Läufe):
|
||||
// systemd-run --scope -q -p MemoryMax=700M -p MemorySwapMax=0 timeout 200 node --max-old-space-size=400 \
|
||||
// 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.
|
||||
'use strict'
|
||||
const { createRequire } = require('module')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const http = require('http')
|
||||
const req = createRequire('/root/.npm/_npx/db89d7302a373f10/node_modules/')
|
||||
const { chromium } = req('playwright-core')
|
||||
|
||||
const repo = path.resolve(__dirname, '../..')
|
||||
const distArg = process.argv[2]
|
||||
const outArg = process.argv[3]
|
||||
if (!distArg || !outArg) { console.error('usage: node shot.cjs <distDir> <outDir>'); process.exit(2) }
|
||||
const dist = path.resolve(distArg)
|
||||
const out = path.resolve(outArg)
|
||||
const tmp = path.resolve(process.env.TMPDIR || os.tmpdir())
|
||||
const okBase = [tmp, path.join(repo, '.superpowers')]
|
||||
if (!okBase.some((b) => out.startsWith(b + path.sep))) { console.error('refusing: outDir must be below ' + okBase.join(' or ')); process.exit(2) }
|
||||
if (fs.existsSync(out) && fs.readdirSync(out).length > 0) { console.error('refusing: outDir must be new or empty'); process.exit(2) }
|
||||
if (!fs.existsSync(path.join(dist, 'index.html'))) { console.error('refusing: no index.html in ' + dist); process.exit(2) }
|
||||
fs.mkdirSync(out, { recursive: true })
|
||||
|
||||
// Namen und Modelle aus der gemeinsamen Roster-Fixture (CommonJS kann den ESM-Quelltext nicht laden)
|
||||
const roster = JSON.parse(fs.readFileSync(path.join(repo, 'tests/fixtures/workers-roster.json'), 'utf8')).roles
|
||||
const MODEL = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' }
|
||||
const FONT_URL = '/fonts/PixelifySans-Variable.woff2'
|
||||
const CHROME = '/root/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell'
|
||||
const HALT_LINE = 'Versand gestoppt: Zwei harte Bounces in den letzten 10 Mails. Ein Admin kann ihn in den Einstellungen freigeben.'
|
||||
|
||||
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8' }
|
||||
const fontLog = []
|
||||
|
||||
function serve() {
|
||||
const server = http.createServer((rq, rs) => {
|
||||
let rel
|
||||
try { rel = decodeURIComponent(new URL(rq.url, 'http://x').pathname) } catch { rs.writeHead(400).end(); return }
|
||||
const fonts = rel.startsWith('/fonts/')
|
||||
const root = fonts ? path.join(repo, 'public', 'fonts') : dist
|
||||
const file = path.resolve(root, '.' + (fonts ? rel.slice('/fonts'.length) : (rel === '/' ? '/index.html' : rel)))
|
||||
if (rq.method !== 'GET' || !file.startsWith(root + path.sep) || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
||||
if (fonts) fontLog.push({ url: rel, status: 404 })
|
||||
rs.writeHead(404).end('not found')
|
||||
return
|
||||
}
|
||||
if (fonts) fontLog.push({ url: rel, status: 200 })
|
||||
rs.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-store' })
|
||||
fs.createReadStream(file).pipe(rs)
|
||||
})
|
||||
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server)))
|
||||
}
|
||||
|
||||
const problems = []
|
||||
const errors = []
|
||||
const checks = {}
|
||||
const metrics = {}
|
||||
const shots = []
|
||||
|
||||
function check(name, ok, detail) {
|
||||
checks[name] = ok ? 'ok' : 'FEHLER' + (detail === undefined ? '' : ' ' + JSON.stringify(detail))
|
||||
if (!ok) problems.push(name + (detail === undefined ? '' : ' ' + JSON.stringify(detail)))
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
const textOf = async (loc) => ((await loc.textContent()) || '').replace(/\s+/g, ' ').trim()
|
||||
|
||||
async function shoot(page, name, options = {}) {
|
||||
const file = path.join(out, name)
|
||||
await page.screenshot({ path: file, ...options })
|
||||
shots.push(file)
|
||||
}
|
||||
|
||||
// Neuer Kontext mit Fehlersammlung; rAF wird mitgezählt (eine Schleife = höchstens ein wartender Rückruf)
|
||||
async function newPage(browser, label, contextOptions) {
|
||||
const context = await browser.newContext(contextOptions)
|
||||
await context.addInitScript(() => {
|
||||
const raf = window.requestAnimationFrame.bind(window)
|
||||
const caf = window.cancelAnimationFrame.bind(window)
|
||||
const waiting = new Set()
|
||||
window.__raf = { waiting, calls: 0 }
|
||||
window.requestAnimationFrame = (cb) => {
|
||||
window.__raf.calls += 1
|
||||
const id = raf((t) => { waiting.delete(id); cb(t) })
|
||||
waiting.add(id)
|
||||
return id
|
||||
}
|
||||
window.cancelAnimationFrame = (id) => { waiting.delete(id); caf(id) }
|
||||
})
|
||||
const page = await context.newPage()
|
||||
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()) })
|
||||
page.on('requestfailed', (r) => errors.push(label + ' requestfailed: ' + r.url()))
|
||||
page.on('response', (r) => {
|
||||
if (r.url().endsWith(FONT_URL)) metrics.fontStatus = r.status()
|
||||
if (r.status() >= 400) errors.push(label + ' http ' + r.status() + ': ' + r.url())
|
||||
})
|
||||
return { context, page }
|
||||
}
|
||||
|
||||
async function ready(page, url) {
|
||||
await page.goto(url)
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-wk-worker]').length === 19)
|
||||
await page.evaluate(() => document.fonts.load('600 12px "Pixelify Sans"'))
|
||||
await page.waitForFunction(() => window.__wk && window.__wk.stage.current && window.__wk.stage.current.stats().frames > 0)
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
}
|
||||
|
||||
const stats = (page) => page.evaluate(() => window.__wk.stage.current.stats())
|
||||
const topOfPage = (page) => page.evaluate(() => window.scrollTo(0, 0))
|
||||
|
||||
async function pickFromList(page, id, name) {
|
||||
await page.locator(`[data-wk-worker="${id}"]`).click()
|
||||
await page.waitForFunction((n) => (document.querySelector('.wk-name') || {}).textContent === n, name)
|
||||
}
|
||||
|
||||
// ---------- Szenario A: Desktop 1280 x 900 ----------
|
||||
|
||||
async function desktop(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'desktop', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
const t0 = Date.now()
|
||||
await ready(page, base + '/index.html')
|
||||
await sleep(500)
|
||||
await topOfPage(page)
|
||||
|
||||
// 1. Schrift: geladen, Status 200, document.fonts.check
|
||||
check('schrift geladen', await page.evaluate(() => document.fonts.check('600 12px "Pixelify Sans"')))
|
||||
check('schrift mit 200 geliefert', metrics.fontStatus === 200, metrics.fontStatus)
|
||||
check('keine Warnzeile ohne ?halted=1', (await page.locator('.msg.err[role="alert"]').count()) === 0)
|
||||
|
||||
// a) erstes Bild: Hochhaus komplett (Bühne mindestens 560 px breit, Zoom 1)
|
||||
const box = await page.locator('.wk-canvas').boundingBox()
|
||||
const first = await stats(page)
|
||||
metrics.desktopStage = { width: Math.round(box.width), height: Math.round(box.height), top: Math.round(box.y), zoom: first.zoom }
|
||||
check('desktop: ganzes Hochhaus im Bild (Zoom 1, Bühne >= 560 px, Unterkante im Fenster)', first.zoom === 1 && box.width >= 560 && box.y + box.height <= 900, metrics.desktopStage)
|
||||
await shoot(page, 'desktop-start.png')
|
||||
|
||||
// 2. 19 Knöpfe; Klick auf jeden zeigt den Namen aus ROLES und den vollen Modellnamen
|
||||
check('19 Knöpfe', (await page.locator('[data-wk-worker]').count()) === 19)
|
||||
const wrong = []
|
||||
for (const r of roster) {
|
||||
await pickFromList(page, r.workerId, r.name)
|
||||
const name = await textOf(page.locator('.wk-name'))
|
||||
const chip = await textOf(page.locator('.wk-chip'))
|
||||
if (name !== r.name || chip !== MODEL[r.model]) wrong.push({ id: r.workerId, name, chip })
|
||||
}
|
||||
check('Klick auf jeden Knopf: Name und Modell-Chip stimmen', wrong.length === 0, wrong)
|
||||
|
||||
// 3. sechs Figuren auf der Leinwand anklicken
|
||||
await page.locator('button[aria-label="Karte schließen"]').click()
|
||||
const hits = []
|
||||
for (const id of ['otto', 'kim', 'rita', 'bea', 'bob', 'arne']) {
|
||||
await topOfPage(page)
|
||||
const pos = await page.evaluate((who) => window.__wk.stage.current.locate(who), id)
|
||||
const cb = await page.locator('.wk-canvas').boundingBox()
|
||||
if (!pos) { hits.push({ id, locate: null }); continue }
|
||||
await page.mouse.click(cb.x + pos.x, cb.y + pos.y)
|
||||
const want = roster.find((r) => r.workerId === id).name
|
||||
let got = ''
|
||||
try {
|
||||
await page.waitForFunction((n) => (document.querySelector('.wk-name') || {}).textContent === n, want, { timeout: 3000 })
|
||||
got = want
|
||||
} catch {
|
||||
got = await textOf(page.locator('.wk-name').first()).catch(() => '')
|
||||
}
|
||||
if (got !== want) hits.push({ id, want, got })
|
||||
}
|
||||
check('Klick auf sechs Figuren trifft die Figur (locate)', hits.length === 0, hits)
|
||||
|
||||
// 4. Spur von kim: javascript:-Zeile nur Text, https-Zeile ein sicherer Link
|
||||
await pickFromList(page, 'kim', 'Kim')
|
||||
await page.waitForFunction(() => document.querySelectorAll('.wk-trace li').length === 12)
|
||||
check('Spur hat 12 Zeilen', (await page.locator('.wk-trace li').count()) === 12)
|
||||
check('kein Link mit javascript:', (await page.locator('.wk-trace a[href^="javascript:" i]').count()) === 0)
|
||||
const fund = page.locator('.wk-trace a[href="https://example.org/fund"]')
|
||||
check('https-Zeile ist ein Link', (await fund.count()) === 1)
|
||||
if ((await fund.count()) === 1) {
|
||||
check('https-Link: rel noopener noreferrer, target _blank', (await fund.getAttribute('rel')) === 'noopener noreferrer' && (await fund.getAttribute('target')) === '_blank')
|
||||
}
|
||||
const unsafeRow = page.locator('.wk-trace li', { hasText: 'Link mit Skript abgelehnt' })
|
||||
check('javascript:-Zeile steht als Text', (await unsafeRow.count()) === 1 && (await unsafeRow.locator('a').count()) === 0)
|
||||
|
||||
// Karte offen (Bea: Sonnet-Chip, Spur) für das Bild
|
||||
await pickFromList(page, 'bea', 'Bea')
|
||||
await page.waitForFunction(() => document.querySelectorAll('.wk-trace li').length === 12)
|
||||
await topOfPage(page)
|
||||
await shoot(page, 'desktop-karte.png')
|
||||
await page.locator('button[aria-label="Karte schließen"]').click()
|
||||
|
||||
// 5. Pausenknopf und Einstellungen
|
||||
const pauseBtn = page.getByRole('button', { name: 'Alles anhalten', exact: true })
|
||||
await pauseBtn.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()
|
||||
await page.getByRole('button', { name: 'Alles anhalten', exact: true }).waitFor()
|
||||
check('Pausenknopf: zurück auf „Alles anhalten“', true)
|
||||
|
||||
await page.getByRole('button', { name: 'Einstellungen', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Hustler-Einstellungen' })
|
||||
await dialog.waitFor()
|
||||
await dialog.getByLabel('Tagesgrenze (USD)').waitFor()
|
||||
check('Einstellungen: Dialog „Hustler-Einstellungen“ mit Feld „Tagesgrenze (USD)“', true)
|
||||
check('Einstellungen ohne Notbremse: kein Block „Versand gestoppt“', (await page.locator('.wk-halt').count()) === 0)
|
||||
await sleep(600) // der Dialog blendet ein
|
||||
await shoot(page, 'desktop-einstellungen.png')
|
||||
await dialog.getByRole('button', { name: 'Abbrechen', exact: true }).click()
|
||||
await page.getByRole('dialog').waitFor({ state: 'detached' })
|
||||
check('Einstellungen: „Abbrechen“ schließt den Dialog', true)
|
||||
|
||||
// 6. nach 20 s Skriptlauf: Bild, Zeichenzeit, Bilder pro Sekunde, eine einzige Schleife
|
||||
await topOfPage(page)
|
||||
const wait = 20000 - (Date.now() - t0)
|
||||
if (wait > 0) await sleep(wait)
|
||||
await shoot(page, 'desktop-nach-20s.png')
|
||||
const timed = () => page.evaluate(() => ({ ...window.__wk.stage.current.stats(), at: performance.now() }))
|
||||
// Messfenster von zwei Sekunden: Bilder je Sekunde und wartende rAF-Rückrufe (eine Schleife = höchstens einer)
|
||||
const sample = async () => {
|
||||
const a = await timed()
|
||||
const raf = []
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
raf.push(await page.evaluate(() => window.__raf.waiting.size))
|
||||
await sleep(100)
|
||||
}
|
||||
const b = await timed()
|
||||
const seconds = (b.at - a.at) / 1000
|
||||
return { frames: b.frames - a.frames, seconds, fps: (b.frames - a.frames) / seconds, avgDrawMs: b.avgDrawMs, rafWaitingMax: Math.max(...raf), raf }
|
||||
}
|
||||
const busy = await sample()
|
||||
const dbg = await page.evaluate(() => window.__wk.stage.current.debug())
|
||||
metrics.fpsDuringScript = Math.round(busy.fps * 10) / 10
|
||||
metrics.avgDrawMs = Math.round(busy.avgDrawMs * 100) / 100
|
||||
metrics.rafWaitingMax = busy.rafWaitingMax
|
||||
metrics.pending = dbg.pending
|
||||
if (!(busy.avgDrawMs <= 25)) console.warn('WARNUNG: avgDrawMs ' + busy.avgDrawMs.toFixed(2) + ' > 25 (software-gerendert)')
|
||||
check('Schleife läuft (Bilder wachsen)', busy.frames > 0, busy.frames)
|
||||
check('StrictMode: genau eine Schleife (höchstens ein wartender rAF-Rückruf)', busy.rafWaitingMax <= 1, busy.raf)
|
||||
check('debug().pending >= 0', dbg.pending >= 0, dbg.pending)
|
||||
// Nach dem Skript (Ende bei 45,8 s, letzter Zustandsabruf bei 45 s) kommen keine Daten mehr: dann zählt nur die Schleife.
|
||||
// Beim Eintreffen von Zustand oder Ereignissen zeichnet die Szene sofort ein Bild extra, das ist keine Schleife.
|
||||
const rest = 49000 - (Date.now() - t0)
|
||||
if (rest > 0) await sleep(rest)
|
||||
const idle = await sample()
|
||||
metrics.fpsIdle = Math.round(idle.fps * 10) / 10
|
||||
metrics.fpsWindowSeconds = Math.round(idle.seconds * 100) / 100
|
||||
// FRAME_MS 33 ergibt höchstens 30,3 Bilder je Sekunde; ein Bild Messkante kommt dazu
|
||||
check('Bilder pro Sekunde höchstens 30 (FRAME_MS 33, Ruhe nach dem Skript)', idle.fps <= 1000 / 33 + 1 / idle.seconds, { fps: metrics.fpsIdle, window: metrics.fpsWindowSeconds })
|
||||
check('Schleife läuft auch in Ruhe mit etwa 30 Bildern je Sekunde', idle.fps >= 20, idle.fps)
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario B: Blasen, Zeichen und Symbole bei doppelter Auflösung ----------
|
||||
|
||||
async function bubbles(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'blasen', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 })
|
||||
await ready(page, base + '/index.html')
|
||||
await sleep(400)
|
||||
await topOfPage(page)
|
||||
const cb = await page.locator('.wk-canvas').boundingBox()
|
||||
const clip = { x: cb.x, y: cb.y, width: cb.width, height: cb.height }
|
||||
await shoot(page, 'desktop-hochhaus-2x.png', { clip })
|
||||
// Echte Engine, selbst gebaute Ereignisse: ’ ü % und die längste erlaubte Blase (20 Zeichen) gleichzeitig
|
||||
const at = Date.now()
|
||||
const make = (n, workerId, kind, text, extra) => ({ id: 'probe-' + n, ts: new Date(at + n).toISOString(), workerId, kind, text, link: null, to: null, prospectId: null, costUsd: 0, meta: null, ...extra })
|
||||
await page.evaluate((events) => window.__wk.stage.current.pushEvents(events), [
|
||||
make(1, 'otto', 'budget_warnung', 'Tagesbudget zu 80 % erreicht'),
|
||||
make(2, 'bob', 'bau_start', 'Vorschau wird gebaut'),
|
||||
make(3, 'theo', 'gelesen', 'Posteingang gelesen', { meta: { count: 123456789012 } }),
|
||||
])
|
||||
await sleep(700)
|
||||
await shoot(page, 'desktop-blasen-2x.png', { clip })
|
||||
await sleep(2600)
|
||||
await page.evaluate((events) => window.__wk.stage.current.pushEvents(events), [
|
||||
make(4, 'paul', 'pruefung_zurueck', 'Prüfung: Runde 1 zurück', { meta: { round: 1 } }),
|
||||
make(5, 'lea', 'gelesen', 'Läden gelesen', { meta: { count: 123456789012 } }),
|
||||
])
|
||||
await sleep(700)
|
||||
await shoot(page, 'desktop-blasen-2x-b.png', { clip })
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario C: Handy 390 x 844 ----------
|
||||
|
||||
async function phone(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'handy', { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true })
|
||||
await ready(page, base + '/index.html')
|
||||
await sleep(500)
|
||||
const width = await page.evaluate(() => document.documentElement.scrollWidth)
|
||||
metrics.phoneScrollWidth = width
|
||||
check('handy: kein seitliches Scrollen (scrollWidth <= 390)', width <= 390, width)
|
||||
await page.locator('.wk-stage').scrollIntoViewIfNeeded()
|
||||
await sleep(200)
|
||||
const zoomBtn = page.getByRole('button', { name: 'Vergrößern', exact: true })
|
||||
check('handy: Zoom-Knöpfe sichtbar', (await zoomBtn.isVisible()) && (await page.getByRole('button', { name: 'Verkleinern', exact: true }).isVisible()))
|
||||
await shoot(page, 'handy.png')
|
||||
const z0 = (await stats(page)).zoom
|
||||
await zoomBtn.click()
|
||||
await sleep(300)
|
||||
const z1 = (await stats(page)).zoom
|
||||
metrics.phoneZoom = [Math.round(z0 * 100) / 100, Math.round(z1 * 100) / 100]
|
||||
check('handy: „+“ erhöht den Zoom', z1 > z0, [z0, z1])
|
||||
await shoot(page, 'handy-zoom.png')
|
||||
await shoot(page, 'handy-ganz.png', { fullPage: true })
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario D: weniger Bewegung ----------
|
||||
|
||||
async function reduced(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'reduziert', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await page.goto(base + '/index.html')
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
await page.reload()
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-wk-worker]').length === 19)
|
||||
await sleep(2000)
|
||||
const f2 = (await stats(page)).frames
|
||||
await sleep(2300)
|
||||
const f4 = (await stats(page)).frames
|
||||
metrics.reducedFrames = [f2, f4]
|
||||
check('weniger Bewegung: nach 2 s höchstens 5 Bilder', f2 <= 5, f2)
|
||||
check('weniger Bewegung: die Schleife steht (Bildzahl bleibt)', f4 === f2, [f2, f4])
|
||||
await context.close()
|
||||
}
|
||||
|
||||
// ---------- Szenario E: Notbremse (H4) ----------
|
||||
|
||||
async function halted(browser, base) {
|
||||
const { context, page } = await newPage(browser, 'notbremse', { viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 })
|
||||
await ready(page, base + '/index.html?halted=1')
|
||||
await sleep(500)
|
||||
await topOfPage(page)
|
||||
const alerts = page.locator('.msg.err[role="alert"]')
|
||||
await alerts.first().waitFor()
|
||||
check('notbremse: genau eine rote Zeile', (await alerts.count()) === 1, await alerts.count())
|
||||
const line = await textOf(alerts.first())
|
||||
check('notbremse: Text der Warnzeile (Punkt des Grundes nicht doppelt)', line === HALT_LINE, line)
|
||||
await shoot(page, 'desktop-gestoppt.png')
|
||||
|
||||
await page.getByRole('button', { name: 'Einstellungen', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Hustler-Einstellungen' })
|
||||
await dialog.waitFor()
|
||||
const block = page.locator('.wk-halt')
|
||||
await block.waitFor()
|
||||
check('notbremse: Block „Versand gestoppt“ im Dialog', (await textOf(block.locator('.wk-set-title'))) === 'Versand gestoppt')
|
||||
const release = block.getByRole('button', { name: 'Versand wieder freigeben', exact: true })
|
||||
check('notbremse: Knopf „Versand wieder freigeben“', (await release.count()) === 1)
|
||||
await release.click()
|
||||
await page.waitForFunction(() => document.querySelectorAll('.wk-halt').length === 0 && document.querySelectorAll('.msg.err[role="alert"]').length === 0, null, { timeout: 2000 })
|
||||
check('notbremse: binnen 2 s sind Block und Warnzeile weg', true)
|
||||
const saved = await page.evaluate(() => JSON.stringify(window.__wk.api.saved))
|
||||
check('notbremse: gesendet wurde genau [{ sendingHalted: false }]', saved === '[{"sendingHalted":false}]', saved)
|
||||
await context.close()
|
||||
}
|
||||
|
||||
(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)
|
||||
} catch (e) {
|
||||
problems.push('Abbruch: ' + (e && e.stack ? e.stack : String(e)))
|
||||
} finally {
|
||||
await browser.close()
|
||||
server.close()
|
||||
}
|
||||
check('keine pageerror/console.error', errors.length === 0, errors)
|
||||
const ok = problems.length === 0
|
||||
console.log(JSON.stringify({ errors, metrics, checks, shots, fontRequests: fontLog.length }, null, 2))
|
||||
if (!ok) { console.error('FAIL:\n' + problems.join('\n')); process.exit(1) }
|
||||
console.log('OK')
|
||||
})().catch((e) => { console.error('FAIL', e); process.exit(1) })
|
||||
Reference in new issue
Block a user