// Playwright-Checks der Kundenansicht im Dashboard: statischer Server fuer public/ + per page.route gemockte API. // Nicht Teil von npm test. Aufruf: node test/ui/kundenansicht.ui.mjs // Screenshots landen in /root/reports/admin-umzug-ui/. import http from 'node:http' import { readFile, mkdir } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { chromium } from '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs' const pub = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../public') const SHOTS = '/root/reports/admin-umzug-ui' await mkdir(SHOTS, { recursive: true }) const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.webmanifest': 'application/json' } const server = http.createServer(async (req, res) => { const url = new URL(req.url, 'http://x') try { const file = path.join(pub, url.pathname === '/' ? 'dashboard.html' : url.pathname) const data = await readFile(file) res.writeHead(200, { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' }) res.end(data) } catch { res.writeHead(404) res.end('nicht gefunden') } }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const base = `http://127.0.0.1:${server.address().port}` const VIEW = 'a'.repeat(32) const customer = { id: 'c1', code: '1001', name: 'Berta Berg', email: 'b@example.test' } const ME = { neu: { authenticated: true, role: 'admin', viewAs: true, viewId: VIEW, viewExpiresAt: Date.now() + 3600000, admin: { name: 'Andrej', email: 'andrej@webklar.com', isAdmin: false }, customer }, // alte Mitarbeiter-Sitzung (Login im Portal oder Alt-Ansicht ohne Kennung): /me beendet sie, weiter ins Ticketsystem staff: { authenticated: false, reason: 'staff', redirect: `${base}/zurueck-im-ticketsystem.html?von=staff` }, gast: { authenticated: false }, } const STAFF_BLOCKED = { error: 'Mitarbeiter arbeiten im Ticketsystem.', ticketUrl: 'https://ticket.webklar.com' } let scenario = 'neu' let replaceAfterMe = false const seen = { headers: [], ende: 0, chatRead: 0 } async function mockApi(page) { await page.route('**/api/**', async (route) => { const req = route.request() const p = new URL(req.url()).pathname const json = (body, status = 200) => route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) }) if (p === '/api/auth/me') return json(ME[scenario]) // nur Skript-Anfragen zaehlen: Bilder () koennen keinen Header tragen und sind reine Leseanfragen if (['fetch', 'xhr'].includes(req.resourceType())) seen.headers.push(req.headers()['x-kundenansicht'] || null) if (replaceAfterMe && p === '/api/projects') return json({ code: 'view_replaced', kunde: 'Kunde Zwei', error: 'ersetzt' }, 409) if (p === '/api/auth/kundenansicht/ende') { seen.ende += 1 return json({ redirect: `${base}/zurueck-im-ticketsystem.html` }) } if (p === '/api/auth/login') return json(STAFF_BLOCKED, 403) if (p === '/api/chat/read') seen.chatRead += 1 if (p === '/api/projects') return json({ projects: [{ id: 'p1', name: 'Bäckerei Berg', status: 'live' }] }) if (p === '/api/contact') return json({ name: 'WEBklar-Team', responseTime: 'Antwort meist < 1 Werktag' }) if (p === '/api/chat/unread-count') return json({ unread: 0 }) if (p === '/api/chat/messages') return json({ messages: [], unread: 2 }) return json({}) }) } let failed = 0 const check = (name, ok, detail = '') => { console.log(`${ok ? 'OK ' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`) if (!ok) failed += 1 } const browser = await chromium.launch() try { for (const [label, viewport] of [['desktop', { width: 1280, height: 900 }], ['phone', { width: 390, height: 844 }]]) { const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1 }) const page = await ctx.newPage() page.setDefaultTimeout(60000) const errors = [] page.on('pageerror', (e) => errors.push(e.message)) await mockApi(page) const open = async (sc, hash = '') => { scenario = sc seen.headers.length = 0 await page.goto('about:blank') await page.goto(`${base}/dashboard.html${hash}`) await page.waitForFunction(() => document.getElementById('customer-name')?.textContent) await page.waitForTimeout(400) } // 1) neue Kundenansicht: Banner, Kennung an den Anfragen, Chat nicht als gelesen markieren, Beenden replaceAfterMe = false seen.chatRead = 0 await open('neu', '#chat') const banner = (await page.textContent('#viewas-banner')).replace(/\s+/g, ' ').trim() check(`${label} banner_text`, banner === 'Kundenansicht: Berta Berg · geöffnet von Andrej Beenden', banner) check(`${label} kennung_an_anfragen`, seen.headers.length > 0 && seen.headers.every((h) => h === VIEW), JSON.stringify(seen.headers)) check(`${label} chat_nicht_gelesen`, seen.chatRead === 0, `chat/read ${seen.chatRead}x`) await page.screenshot({ path: `${SHOTS}/${label}-banner.png` }) await Promise.all([page.waitForURL(/zurueck-im-ticketsystem/), page.click('#exit-viewas-btn')]) check(`${label} beenden_ruft_ende_und_leitet_um`, seen.ende >= 1) // 2) ersetzte Ansicht: Overlay statt Inhalt replaceAfterMe = true await open('neu') await page.waitForSelector('#kb-view-overlay', { timeout: 15000 }).catch(() => null) const overlay = await page.textContent('#kb-view-overlay').catch(() => '') check(`${label} overlay_ersetzt`, /Diese Ansicht wurde durch die Kundenansicht von Kunde Zwei ersetzt\./.test(overlay || ''), overlay) await page.screenshot({ path: `${SHOTS}/${label}-ersetzt.png` }) replaceAfterMe = false // 3) alte Mitarbeiter-Sitzung: Dashboard leitet ins Ticketsystem weiter scenario = 'staff' await page.goto('about:blank') await Promise.all([page.waitForURL(/zurueck-im-ticketsystem\.html\?von=staff/), page.goto(`${base}/dashboard.html`)]) check(`${label} staff_weiterleitung`, page.url().endsWith('/zurueck-im-ticketsystem.html?von=staff'), page.url()) // 4) Mitarbeiter meldet sich im Portal an: Hinweis und Link ins Ticketsystem scenario = 'gast' await page.goto(`${base}/login.html`) await page.fill('#email', 'andrej@webklar.com') await page.fill('#password', 'geheim-geheim') await page.click('#login-btn') await page.waitForSelector('#login-staff-link:not(.hidden)', { timeout: 15000 }).catch(() => null) const hint = (await page.textContent('#login-error')).trim() check(`${label} login_hinweis`, hint === 'Mitarbeiter arbeiten im Ticketsystem.', hint) const link = await page.$eval('#login-staff-link', (a) => ({ text: a.textContent.trim(), href: a.getAttribute('href'), visible: a.offsetParent !== null })) check(`${label} login_link_ticketsystem`, link.visible && link.text === 'Zum Ticketsystem' && link.href === 'https://ticket.webklar.com', JSON.stringify(link)) check(`${label} kein_mfa_formular`, (await page.$('#mfa-form')) === null) await page.waitForTimeout(1200) await page.screenshot({ path: `${SHOTS}/${label}-login-mitarbeiter.png` }) check(`${label} keine_js_fehler`, errors.length === 0, errors.join(' | ')) await ctx.close() } } finally { await browser.close() server.close() } console.log(failed ? `${failed} Check(s) fehlgeschlagen` : 'alle Checks ok') process.exit(failed ? 1 : 0)