Einbau-Skript news.js für Kundenwebsites
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a09d060a1
commit
035d1d5517
5 files changed
+547
No files matched your search
@@ -0,0 +1,204 @@
|
||||
// Playwright-Checks Einbau-Skript public/embed/news.js: statischer Server fuer public/ + per page.route gemockte API.
|
||||
// Nicht Teil von npm test. Aufruf: node test/ui/embed.ui.mjs
|
||||
// Screenshots landen in /root/reports/funktionen-news-ui/embed-*.png.
|
||||
import http from 'node:http'
|
||||
import { readFile, mkdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const PW = process.env.PLAYWRIGHT_CORE || '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
|
||||
const { chromium } = await import(PW)
|
||||
|
||||
const pub = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../public')
|
||||
const SHOTS = '/root/reports/funktionen-news-ui'
|
||||
await mkdir(SHOTS, { recursive: true })
|
||||
const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' }
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://x')
|
||||
try {
|
||||
const file = path.join(pub, 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 DEMO = `${base}/embed/news-demo.html`
|
||||
|
||||
// ---- Fixtures ----
|
||||
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')
|
||||
const PAPERS = ['sun', 'sky', 'coral', 'white']
|
||||
const KINDS = ['combo', 'note', 'photo', 'note', 'combo', 'note', 'photo', 'note']
|
||||
function makePosts(n, offset = 0) {
|
||||
return Array.from({ length: n }, (_, k) => {
|
||||
const i = k + offset
|
||||
const type = KINDS[i % KINDS.length]
|
||||
return {
|
||||
id: `p${i}`, type, title: i === 3 ? '' : `Beitrag ${i + 1}`, text: type === 'photo' ? '' : `Text zu Beitrag ${i + 1}. Frisch reingekommen, komm vorbei.`,
|
||||
paper: PAPERS[i % 4], date: '2026-09-29',
|
||||
image: type === 'note' ? null : { w: 1200, h: 900, src: `/api/public/news/abcdefghijklmnop/img/p${i}.webp?v=1`, srcSmall: `/api/public/news/abcdefghijklmnop/img/p${i}.webp?v=1&s=1` },
|
||||
}
|
||||
})
|
||||
}
|
||||
async function mockApi(page, { total = 0, status = 200, custom = null } = {}) {
|
||||
await page.route('**/api/public/news/**', async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.pathname.includes('/img/')) return route.fulfill({ status: 200, contentType: 'image/png', body: PNG })
|
||||
if (status !== 200) return route.fulfill({ status, contentType: 'application/json', body: '{"error":"down"}' })
|
||||
if (custom) return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ posts: custom, hasMore: false }) })
|
||||
const pg = Number(url.searchParams.get('page') || 1)
|
||||
const all = makePosts(total)
|
||||
const posts = all.slice((pg - 1) * 6, pg * 6)
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ posts, hasMore: pg * 6 < total }) })
|
||||
})
|
||||
}
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const results = []
|
||||
async function check(name, fn, { viewport = { width: 1280, height: 900 }, javaScriptEnabled = true } = {}) {
|
||||
const ctx = await browser.newContext({ viewport, javaScriptEnabled })
|
||||
const page = await ctx.newPage()
|
||||
const errors = []
|
||||
page.on('pageerror', (e) => errors.push(e.message))
|
||||
try {
|
||||
await fn(page, errors)
|
||||
results.push([name, true])
|
||||
console.log(`ok ${name}`)
|
||||
} catch (e) {
|
||||
results.push([name, false])
|
||||
console.log(`FAIL ${name}: ${e.message}`)
|
||||
}
|
||||
await ctx.close()
|
||||
}
|
||||
function assert(cond, msg) { if (!cond) throw new Error(msg) }
|
||||
|
||||
await check('leer_bleibt_leerzustand', async (page, errors) => {
|
||||
await mockApi(page, { total: 0 })
|
||||
await page.goto(DEMO)
|
||||
await page.waitForLoadState('networkidle')
|
||||
assert(await page.locator('#static-empty').isVisible(), 'Leerzustand nicht sichtbar')
|
||||
assert((await page.locator('.wkn-board').count()) === 0, 'Brett trotz 0 Posts')
|
||||
assert((await page.locator('#wkn-style').count()) <= 1, 'Style mehrfach')
|
||||
assert(errors.length === 0, errors.join())
|
||||
})
|
||||
|
||||
await check('ein_beitrag_neu_schild', async (page) => {
|
||||
await mockApi(page, { total: 1 })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
assert((await page.locator('.wkn-pin').count()) === 1, 'nicht genau 1 Zettel')
|
||||
assert((await page.locator('.wkn-new').count()) === 1, 'Neu-Schild fehlt')
|
||||
assert((await page.locator('.wkn-new').innerText()) === 'Neu', 'Schild-Text')
|
||||
assert((await page.locator('#static-empty').count()) === 0, 'Leerzustand blieb stehen')
|
||||
assert((await page.locator('.wkn-more').isHidden()), 'Knopf trotz hasMore=false sichtbar')
|
||||
assert((await page.locator('#wkn-style').count()) === 1, 'Style nicht genau einmal')
|
||||
assert((await page.locator('.wkn-date').first().innerText()).toLowerCase() === '29. sep', 'Datum: ' + await page.locator('.wkn-date').first().innerText())
|
||||
})
|
||||
|
||||
await check('acht_beitraege_aeltere', async (page) => {
|
||||
await mockApi(page, { total: 8 })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
assert((await page.locator('.wkn-pin').count()) === 6, '6 erwartet')
|
||||
const btn = page.getByRole('button', { name: 'Ältere Neuigkeiten ansehen' })
|
||||
assert(await btn.isVisible(), 'Knopf fehlt')
|
||||
await btn.click()
|
||||
await page.waitForFunction(() => document.querySelectorAll('.wkn-pin').length === 8)
|
||||
assert(await btn.isHidden(), 'Knopf nicht verschwunden')
|
||||
assert((await page.locator('.wkn-new').count()) === 1, 'Neu nur am neuesten')
|
||||
})
|
||||
|
||||
await check('xss_als_text', async (page, errors) => {
|
||||
const evil = '<img src=x onerror=window.__x=1>'
|
||||
await mockApi(page, { custom: [{ id: 'x', type: 'note', title: evil, text: evil, paper: 'sun', date: '2026-09-29', image: null }] })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
await page.waitForTimeout(300)
|
||||
assert((await page.locator('.wkn-pin h3').innerText()) === evil, 'Titel nicht als Text')
|
||||
assert((await page.locator('.wkn-pin img').count()) === 0, 'img-Element eingeschleust')
|
||||
assert((await page.evaluate(() => window.__x)) === undefined, 'window.__x gesetzt')
|
||||
})
|
||||
|
||||
await check('api_down_kein_fehler', async (page, errors) => {
|
||||
await mockApi(page, { status: 503 })
|
||||
await page.goto(DEMO)
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.waitForTimeout(200)
|
||||
assert(await page.locator('#static-empty').isVisible(), 'Leerzustand nicht sichtbar')
|
||||
assert(errors.length === 0, 'Exception: ' + errors.join())
|
||||
})
|
||||
|
||||
await check('ohne_js', async (page) => {
|
||||
await mockApi(page, { total: 3 })
|
||||
await page.goto(DEMO)
|
||||
assert(await page.locator('#static-empty').isVisible(), 'Leerzustand nicht sichtbar')
|
||||
}, { javaScriptEnabled: false })
|
||||
|
||||
await check('handy_wischreihe', async (page) => {
|
||||
await mockApi(page, { total: 6 })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
const info = await page.evaluate(() => {
|
||||
const g = document.querySelector('.wkn-grid')
|
||||
const cs = getComputedStyle(g)
|
||||
return { snap: cs.scrollSnapType, ox: cs.overflowX, flex: cs.display, gw: g.scrollWidth, cw: g.clientWidth, doc: document.documentElement.scrollWidth, win: window.innerWidth, hint: getComputedStyle(document.querySelector('.wkn-swipehint')).display, dots: document.querySelectorAll('.wkn-dots i').length }
|
||||
})
|
||||
assert(/x\s+mandatory/.test(info.snap), 'kein Scroll-Snap: ' + info.snap)
|
||||
assert(info.ox === 'auto' && info.flex === 'flex', 'keine Reihe')
|
||||
assert(info.gw > info.cw, 'Reihe scrollt nicht')
|
||||
assert(info.doc <= info.win, `Seite scrollt horizontal (${info.doc} > ${info.win})`)
|
||||
assert(info.hint === 'flex' && info.dots === 6, 'Wisch-Hinweis/Punkte')
|
||||
}, { viewport: { width: 390, height: 844 } })
|
||||
|
||||
await check('keine_cookies_storage', async (page) => {
|
||||
await mockApi(page, { total: 8 })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
const s = await page.evaluate(() => ({ c: document.cookie, l: localStorage.length, s: sessionStorage.length }))
|
||||
assert(s.c === '' && s.l === 0 && s.s === 0, JSON.stringify(s))
|
||||
})
|
||||
|
||||
await check('signatur_optional', async (page) => {
|
||||
await mockApi(page, { total: 2 })
|
||||
await page.route('**/embed/news-demo.html', async (route) => {
|
||||
const r = await route.fetch()
|
||||
await route.fulfill({ response: r, body: (await r.text()).replace('data-webklar-news="abcdefghijklmnop"', 'data-webklar-news="abcdefghijklmnop" data-signature="Opa Martin"') })
|
||||
})
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
assert((await page.locator('.wkn-sig').count()) > 0, 'Signatur fehlt')
|
||||
assert((await page.locator('.wkn-sig').first().innerText()) === 'Opa Martin', 'Signaturtext')
|
||||
})
|
||||
|
||||
await check('ohne_titel_keine_ueberschrift', async (page) => {
|
||||
await mockApi(page, { custom: [{ id: 'n', type: 'note', title: '', text: 'Nur Text', paper: 'sky', date: '2026-09-29', image: null }] })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
assert((await page.locator('.wkn-pin h3').count()) === 0, 'leere Ueberschrift')
|
||||
assert((await page.locator('.wkn-sig').count()) === 0, 'Signatur ohne Attribut')
|
||||
})
|
||||
|
||||
// Screenshots
|
||||
for (const [label, viewport] of [['desktop', { width: 1280, height: 900 }], ['phone', { width: 390, height: 844 }]]) {
|
||||
for (const n of [1, 8]) {
|
||||
const ctx = await browser.newContext({ viewport })
|
||||
const page = await ctx.newPage()
|
||||
await mockApi(page, { total: n })
|
||||
await page.goto(DEMO)
|
||||
await page.locator('.wkn-pin').first().waitFor()
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.screenshot({ path: `${SHOTS}/embed-${label}-${n}.png`, fullPage: true })
|
||||
await ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
server.close()
|
||||
const failed = results.filter(([, ok]) => !ok)
|
||||
console.log(`${results.length - failed.length}/${results.length} Checks bestanden`)
|
||||
process.exit(failed.length ? 1 : 0)
|
||||
Reference in new issue
Block a user