Kundenansicht im Dashboard: Kennung bei jeder Anfrage, Banner mit Beenden, Hinweis bei ersetzter oder abgelaufener Ansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c5afd42a5f
commit
c1fda8a717
6 files changed
+443
-5
No files matched your search
+26
-5
@@ -634,6 +634,19 @@ function renderViewAsBanner(me) {
|
||||
const banner = document.getElementById('viewas-banner')
|
||||
if (!banner || !me.viewAs) return
|
||||
banner.classList.remove('hidden')
|
||||
// Kundenansicht aus dem Ticketsystem (mit Kennung): Beenden fuehrt zurueck auf die Kundenseite im Ticketsystem
|
||||
if (me.viewId) {
|
||||
banner.innerHTML = `
|
||||
<span>Kundenansicht: <strong>${escapeHtml(me.customer?.name || 'Kunde')}</strong> · geöffnet von ${escapeHtml(me.admin?.name || 'Mitarbeiter')}</span>
|
||||
<button type="button" id="exit-viewas-btn">Beenden</button>
|
||||
`
|
||||
document.getElementById('exit-viewas-btn')?.addEventListener('click', async () => {
|
||||
const { redirect } = await api('/api/auth/kundenansicht/ende', { method: 'POST' }).catch(() => ({}))
|
||||
window.location.href = redirect || 'https://ticket.webklar.com/customers'
|
||||
})
|
||||
return
|
||||
}
|
||||
// Alte Ansicht "Als Kunde ansehen" aus dem Admin-Panel (bis das Panel abgeschaltet ist)
|
||||
banner.innerHTML = `
|
||||
<span>Admin-Ansicht: Du siehst das Portal als <strong>${escapeHtml(me.customer?.name || 'Kunde')}</strong>.</span>
|
||||
<button type="button" id="exit-viewas-btn">Zurück zur Admin-Übersicht</button>
|
||||
@@ -765,6 +778,12 @@ function updateChatBadge(unread) {
|
||||
}
|
||||
}
|
||||
|
||||
// Nachrichten als gelesen markieren; in der Kundenansicht nicht (der Kunde hat sie ja nicht gelesen)
|
||||
function markChatRead() {
|
||||
if (chatState.viewAs) return Promise.resolve()
|
||||
return api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
}
|
||||
|
||||
async function pollChatMessages() {
|
||||
if (document.hidden) return
|
||||
try {
|
||||
@@ -772,7 +791,7 @@ async function pollChatMessages() {
|
||||
const data = await api(`/api/chat/messages${q}`)
|
||||
appendChatMessages(data.messages || [])
|
||||
if (isChatTabActive() && (data.unread || 0) > 0) {
|
||||
await api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
await markChatRead()
|
||||
updateChatBadge(0)
|
||||
}
|
||||
} catch {
|
||||
@@ -809,7 +828,7 @@ async function initChat() {
|
||||
})
|
||||
|
||||
await pollChatMessages()
|
||||
await api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
await markChatRead()
|
||||
updateChatBadge(0)
|
||||
chatState.pollTimer = setInterval(() => {
|
||||
if (isChatTabActive()) pollChatMessages()
|
||||
@@ -2004,7 +2023,7 @@ function showView(name, { updateHash = true } = {}) {
|
||||
if (box) box.innerHTML = `<p class="muted">${escapeHtml(err.message || 'Konnte nicht geladen werden.')}</p>`
|
||||
})
|
||||
if (name === 'chat') {
|
||||
api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
markChatRead()
|
||||
updateChatBadge(0)
|
||||
}
|
||||
if (updateHash) {
|
||||
@@ -2070,8 +2089,8 @@ async function initDashboardPage() {
|
||||
|
||||
if (!me.authenticated) {
|
||||
const target = new URL(loginUrl, window.location.origin)
|
||||
// Passwort auf einem anderen Gerät geändert: Hinweis auf der Anmeldeseite
|
||||
if (me.reason === 'password_changed' && me.message) target.searchParams.set('error', me.message)
|
||||
// Passwort auf einem anderen Gerät geändert oder Kundenansicht abgelaufen: Hinweis auf der Anmeldeseite
|
||||
if ((me.reason === 'password_changed' || me.reason === 'view_expired') && me.message) target.searchParams.set('error', me.message)
|
||||
window.location.href = target.pathname + target.search
|
||||
return
|
||||
}
|
||||
@@ -2085,6 +2104,8 @@ async function initDashboardPage() {
|
||||
}
|
||||
|
||||
const customer = me.customer
|
||||
// Kundenansicht: ab jetzt traegt jede /api-Anfrage die Kennung dieser Ansicht (public/kundenansicht.js)
|
||||
if (me.viewId) window.KbView?.setId(me.viewId)
|
||||
chatState.viewAs = Boolean(me.viewAs)
|
||||
takeoverState.customer = customer
|
||||
renderViewAsBanner(me)
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Kundenansicht: Kennung an jeder /api-Anfrage, Hinweis bei ersetzter/abgelaufener Ansicht (vor allen anderen Skripten) -->
|
||||
<script src="/kundenansicht.js"></script>
|
||||
<script src="/chat-composer.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* Kundenansicht (Mitarbeiter oeffnet das Portal eines Kunden aus dem Ticketsystem).
|
||||
Ein Browser haelt nur eine Portal-Sitzung: oeffnet jemand Kunde B, waehrend Kunde A noch in einem Tab offen ist,
|
||||
darf der Tab von A keine Daten von B laden. Deshalb traegt jede eigene /api-Anfrage die Kennung der Ansicht
|
||||
(X-Kundenansicht); der Server antwortet bei fremder Kennung mit 409 view_replaced, nach 2 Stunden mit
|
||||
401 view_expired. Dann zeigt dieses Skript nur noch einen Hinweis. Klassisches Skript, vor app.js laden. */
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
const HEADER = 'X-Kundenansicht'
|
||||
let viewId = ''
|
||||
|
||||
function isOwnApi(url) {
|
||||
try {
|
||||
const u = new URL(String(url), window.location.href)
|
||||
return u.origin === window.location.origin && u.pathname.startsWith('/api/')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function overlay(text) {
|
||||
const existing = document.getElementById('kb-view-overlay-text')
|
||||
if (existing) {
|
||||
existing.textContent = text
|
||||
return
|
||||
}
|
||||
const box = document.createElement('div')
|
||||
box.id = 'kb-view-overlay'
|
||||
box.className = 'kb-view-overlay'
|
||||
box.setAttribute('role', 'alertdialog')
|
||||
box.setAttribute('aria-modal', 'true')
|
||||
const card = document.createElement('div')
|
||||
card.className = 'kb-view-overlay-card'
|
||||
const p = document.createElement('p')
|
||||
p.id = 'kb-view-overlay-text'
|
||||
p.textContent = text
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.textContent = 'Tab schließen'
|
||||
button.addEventListener('click', () => window.close())
|
||||
card.appendChild(p)
|
||||
card.appendChild(button)
|
||||
box.appendChild(card)
|
||||
document.body.appendChild(box)
|
||||
}
|
||||
|
||||
function showReplaced(kunde) {
|
||||
overlay(`Diese Ansicht wurde durch die Kundenansicht von ${kunde || 'einem anderen Kunden'} ersetzt.`)
|
||||
}
|
||||
|
||||
function showExpired() {
|
||||
overlay('Die Kundenansicht ist abgelaufen. Bitte im Ticketsystem erneut öffnen.')
|
||||
}
|
||||
|
||||
function inspect(status, body) {
|
||||
if (status === 409 && body && body.code === 'view_replaced') showReplaced(body.kunde)
|
||||
else if (status === 401 && body && body.code === 'view_expired') showExpired()
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window)
|
||||
window.fetch = async function (input, init) {
|
||||
const url = typeof input === 'string' ? input : (input && input.url) || String(input)
|
||||
const own = isOwnApi(url)
|
||||
let options = init || {}
|
||||
if (viewId && own) {
|
||||
const headers = new Headers(options.headers || (typeof input !== 'string' && input && input.headers) || {})
|
||||
headers.set(HEADER, viewId)
|
||||
options = { ...options, headers }
|
||||
}
|
||||
const response = await originalFetch(input, options)
|
||||
if (own && (response.status === 409 || response.status === 401)) {
|
||||
response.clone().json().then((body) => inspect(response.status, body)).catch(() => {})
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
if (window.XMLHttpRequest) {
|
||||
const proto = window.XMLHttpRequest.prototype
|
||||
const open = proto.open
|
||||
const send = proto.send
|
||||
proto.open = function (method, url, ...rest) {
|
||||
this.kbOwnApi = isOwnApi(url)
|
||||
return open.call(this, method, url, ...rest)
|
||||
}
|
||||
proto.send = function (...args) {
|
||||
if (this.kbOwnApi) {
|
||||
if (viewId) this.setRequestHeader(HEADER, viewId)
|
||||
this.addEventListener('load', () => {
|
||||
if (this.status !== 409 && this.status !== 401) return
|
||||
try {
|
||||
inspect(this.status, JSON.parse(this.responseText))
|
||||
} catch {
|
||||
/* keine JSON-Antwort */
|
||||
}
|
||||
})
|
||||
}
|
||||
return send.apply(this, args)
|
||||
}
|
||||
}
|
||||
|
||||
window.KbView = {
|
||||
setId(id) {
|
||||
viewId = String(id || '')
|
||||
},
|
||||
id: () => viewId,
|
||||
showReplaced,
|
||||
showExpired,
|
||||
}
|
||||
})()
|
||||
@@ -174,6 +174,18 @@
|
||||
}
|
||||
.viewas-banner button { background: transparent; color: #fff; border: 1px solid var(--kb-border-strong); border-radius: 999px; padding: 7px 14px; font: inherit; font-size: 13px; cursor: pointer; }
|
||||
|
||||
/* Kundenansicht ersetzt oder abgelaufen (public/kundenansicht.js): deckt die ganze Seite ab, damit nichts Veraltetes bedienbar bleibt */
|
||||
.kb-view-overlay {
|
||||
position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.78); backdrop-filter: blur(4px);
|
||||
}
|
||||
.kb-view-overlay-card {
|
||||
max-width: 420px; padding: 24px; border-radius: var(--kb-radius, 14px); text-align: center;
|
||||
background: #121216; border: 1px solid rgba(251, 191, 36, 0.45); color: #fff;
|
||||
}
|
||||
.kb-view-overlay-card p { margin: 0 0 18px; line-height: 1.5; }
|
||||
.kb-view-overlay-card button { background: var(--kb-purple, #9d3dfe); color: #fff; border: none; border-radius: 999px; padding: 9px 18px; font: inherit; cursor: pointer; }
|
||||
|
||||
/* Buttons */
|
||||
.kb-btn-primary {
|
||||
display: inline-block; padding: 12px 22px; border-radius: 999px; border: none; background: var(--kb-purple);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
// public/kundenansicht.js im Browser-Nachbau: Kennung X-Kundenansicht an jede eigene /api-Anfrage,
|
||||
// Hinweis-Overlay bei ersetzter (409 view_replaced) oder abgelaufener (401 view_expired) Ansicht
|
||||
const SRC = readFileSync(new URL('../public/kundenansicht.js', import.meta.url), 'utf8')
|
||||
const ID = 'a'.repeat(32)
|
||||
|
||||
function fakeElement(tag) {
|
||||
return {
|
||||
tag,
|
||||
id: '',
|
||||
className: '',
|
||||
textContent: '',
|
||||
type: '',
|
||||
children: [],
|
||||
attrs: {},
|
||||
listeners: {},
|
||||
appendChild(child) {
|
||||
this.children.push(child)
|
||||
return child
|
||||
},
|
||||
setAttribute(name, value) {
|
||||
this.attrs[name] = value
|
||||
},
|
||||
addEventListener(name, fn) {
|
||||
this.listeners[name] = fn
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function browser({ respond = () => ({ status: 200, body: {} }) } = {}) {
|
||||
const calls = []
|
||||
const body = fakeElement('body')
|
||||
const all = () => {
|
||||
const out = []
|
||||
const walk = (n) => {
|
||||
out.push(n)
|
||||
n.children.forEach(walk)
|
||||
}
|
||||
walk(body)
|
||||
return out
|
||||
}
|
||||
const document = {
|
||||
body,
|
||||
createElement: (tag) => fakeElement(tag),
|
||||
getElementById: (id) => all().find((n) => n.id === id) || null,
|
||||
}
|
||||
const xhrs = []
|
||||
class XMLHttpRequest {
|
||||
constructor() {
|
||||
this.headers = {}
|
||||
this.listeners = {}
|
||||
xhrs.push(this)
|
||||
}
|
||||
open(method, url) {
|
||||
this.method = method
|
||||
this.url = url
|
||||
}
|
||||
setRequestHeader(name, value) {
|
||||
this.headers[name] = value
|
||||
}
|
||||
addEventListener(name, fn) {
|
||||
this.listeners[name] = fn
|
||||
}
|
||||
send() {
|
||||
this.sent = true
|
||||
}
|
||||
}
|
||||
const ctx = {
|
||||
URL,
|
||||
Headers,
|
||||
Response,
|
||||
console,
|
||||
document,
|
||||
XMLHttpRequest,
|
||||
location: { href: 'https://project.webklar.com/dashboard.html', origin: 'https://project.webklar.com' },
|
||||
close: () => {
|
||||
ctx.closed = true
|
||||
},
|
||||
fetch: async (input, init = {}) => {
|
||||
calls.push({ url: typeof input === 'string' ? input : input.url, headers: new Headers(init.headers || {}) })
|
||||
const { status, body: json } = respond(input)
|
||||
return new Response(JSON.stringify(json), { status, headers: { 'content-type': 'application/json' } })
|
||||
},
|
||||
}
|
||||
ctx.window = ctx
|
||||
vm.createContext(ctx)
|
||||
vm.runInContext(SRC, ctx)
|
||||
const overlayText = () => {
|
||||
const box = document.getElementById('kb-view-overlay')
|
||||
if (!box) return null
|
||||
const texts = []
|
||||
const walk = (n) => {
|
||||
if (n.textContent) texts.push(n.textContent)
|
||||
n.children.forEach(walk)
|
||||
}
|
||||
walk(box)
|
||||
return texts.join(' | ')
|
||||
}
|
||||
return { ctx, calls, xhrs, overlayText }
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
test('ohne ID kein Header', async () => {
|
||||
const { ctx, calls } = browser()
|
||||
await ctx.fetch('/api/projects')
|
||||
assert.equal(calls[0].headers.get('x-kundenansicht'), null)
|
||||
})
|
||||
|
||||
test('mit ID: Header bei /api/... gleicher Herkunft, nicht bei fremder Herkunft oder /img', async () => {
|
||||
const { ctx, calls } = browser()
|
||||
ctx.KbView.setId(ID)
|
||||
assert.equal(ctx.KbView.id(), ID)
|
||||
await ctx.fetch('/api/projects')
|
||||
await ctx.fetch('https://example.org/api/x')
|
||||
await ctx.fetch('/img/logo.png')
|
||||
await ctx.fetch('https://project.webklar.com/api/news', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
||||
assert.equal(calls[0].headers.get('x-kundenansicht'), ID)
|
||||
assert.equal(calls[1].headers.get('x-kundenansicht'), null)
|
||||
assert.equal(calls[2].headers.get('x-kundenansicht'), null)
|
||||
assert.equal(calls[3].headers.get('x-kundenansicht'), ID)
|
||||
assert.equal(calls[3].headers.get('content-type'), 'application/json', 'vorhandene Header bleiben')
|
||||
})
|
||||
|
||||
test('409 view_replaced zeigt Overlay mit Kundennamen, Antwort bleibt lesbar', async () => {
|
||||
const { ctx, overlayText } = browser({ respond: () => ({ status: 409, body: { code: 'view_replaced', kunde: 'Kunde Zwei', error: 'x' } }) })
|
||||
ctx.KbView.setId(ID)
|
||||
const res = await ctx.fetch('/api/projects')
|
||||
assert.equal((await res.json()).code, 'view_replaced')
|
||||
await settle()
|
||||
assert.match(overlayText(), /Diese Ansicht wurde durch die Kundenansicht von Kunde Zwei ersetzt\./)
|
||||
assert.match(overlayText(), /Tab schließen/)
|
||||
})
|
||||
|
||||
test('401 view_expired zeigt Abgelaufen-Overlay, andere 401 nicht', async () => {
|
||||
const other = browser({ respond: () => ({ status: 401, body: { error: 'Nicht angemeldet' } }) })
|
||||
await other.ctx.fetch('/api/projects')
|
||||
await settle()
|
||||
assert.equal(other.overlayText(), null)
|
||||
const { ctx, overlayText } = browser({ respond: () => ({ status: 401, body: { code: 'view_expired' } }) })
|
||||
await ctx.fetch('/api/projects')
|
||||
await settle()
|
||||
assert.match(overlayText(), /Die Kundenansicht ist abgelaufen\. Bitte im Ticketsystem erneut öffnen\./)
|
||||
})
|
||||
|
||||
test('Overlay-Knopf schliesst den Tab, zweiter Hinweis ersetzt den Text statt zu stapeln', async () => {
|
||||
const { ctx, overlayText } = browser()
|
||||
ctx.KbView.showReplaced('Kunde Zwei')
|
||||
ctx.KbView.showExpired()
|
||||
const box = ctx.document.getElementById('kb-view-overlay')
|
||||
assert.equal(ctx.document.body.children.length, 1)
|
||||
assert.match(overlayText(), /abgelaufen/)
|
||||
const find = (n) => (n.tag === 'button' ? n : n.children.map(find).find(Boolean))
|
||||
find(box).listeners.click()
|
||||
assert.equal(ctx.closed, true)
|
||||
})
|
||||
|
||||
test('XHR bekommt den Header ebenfalls (nur eigene /api-Adressen)', () => {
|
||||
const { ctx, xhrs } = browser()
|
||||
ctx.KbView.setId(ID)
|
||||
const a = new ctx.XMLHttpRequest()
|
||||
a.open('POST', '/api/chat/attachments')
|
||||
a.send()
|
||||
const b = new ctx.XMLHttpRequest()
|
||||
b.open('GET', 'https://example.org/api/x')
|
||||
b.send()
|
||||
assert.equal(xhrs[0].headers['X-Kundenansicht'], ID)
|
||||
assert.equal(xhrs[1].headers['X-Kundenansicht'], undefined)
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
// 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 },
|
||||
alt: { authenticated: true, role: 'admin', viewAs: true, admin: { name: 'Kenso', email: 'kenso@webklar.com', isAdmin: true }, customer },
|
||||
}
|
||||
|
||||
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 (<img src="/api/avatar.svg">) 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/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 Ansicht aus dem Admin-Panel: altes Banner, keine Kennung
|
||||
await open('alt')
|
||||
const old = (await page.textContent('#viewas-banner')).replace(/\s+/g, ' ').trim()
|
||||
check(`${label} altes_banner`, old.includes('Admin-Ansicht') && old.includes('Zurück zur Admin-Übersicht'), old)
|
||||
check(`${label} alt_ohne_kennung`, seen.headers.every((h) => h === null), JSON.stringify(seen.headers))
|
||||
|
||||
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)
|
||||
Reference in new issue
Block a user