Kundenansicht neu: Glas-Logo auf der Anmeldung und Startbild der App
- public/neu/glas-logo.js: das Bauteil „liquid-glass-logo-reveal“ (21st.dev, Shader und Zeitleiste aus zanwei/glass-icon-reveal, MIT, Hinweis im Kopf) in reines JavaScript übertragen: WEBklar-Zeichen als Zeichenfläche (Stern und Punkt ausgestanzt), weiß, keine Schleife, Textur 512 px (auf dem Handy ~3× schneller bereit), Hintergrund läuft zum Rand aus, kein Rauschen auf reinem Weiß, ohne WebGL2 das ruhende Logo als CSS-Maske. - Anmeldung (Handy und PC): Glas-Logo statt App-Icon, der Name erscheint, sobald das Glas losgelassen hat; ein Klick spielt es erneut; reduzierte Bewegung zeigt das ruhende Logo. - Installierte App: startbild.js im Kopf von Übersicht und Anmeldung deckt beim ersten Öffnen je App-Start weiß ab, spielt das Glas-Logo (Tempo 1,25) und blendet dann weich aus; ein Tipp überspringt, spätestens nach 6,5 s ist es weg. Danach ruht das Logo der Anmeldung. - Tests: knGlasLogo (Zeitleiste, Glasbreite < 1,3 Kacheln, MIT-Hinweis, Startbild in der VM), Seitenprüfungen der Anmeldung und der Übersicht kennen die neuen Skripte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1725aaa91c
commit
c997703251
9 files changed
+1221
-10
No files matched your search
@@ -8,11 +8,15 @@ const PAARE = ['login', 'kuendigen', 'widerrufen', 'auftrag', 'offline'].map((n)
|
||||
const koerper = (html) => html.slice(html.search(/<body/i)) // Metas und Links im Kopf ändern sich bewusst (Stil, theme-color), geprüft werden sie unten
|
||||
const attribute = (html) => [...koerper(html).matchAll(/\s(name|id|value|type|autocomplete|maxlength|required|min|max|step|inputmode|placeholder|enterkeyhint|data-text|data-label|data-label-for)(?:="([^"]*)")?/g)].map((m) => `${m[1]}=${m[2] ?? ''}`).sort()
|
||||
const skripte = (html) => [...html.matchAll(/<script[^>]*>[\s\S]*?<\/script>/g)].map((m) => m[0].replace(/\s+/g, ' '))
|
||||
// Eigene Zusätze der neuen Anmeldung, die die alte Seite nicht hat: Glas-Logo (login-logo.js) und Startbild der App (startbild.js)
|
||||
const ZUSAETZE = ['<script src="/neu/startbild.js"></script>', '<script type="module" src="/neu/login-logo.js"></script>']
|
||||
const ohneZusaetze = (html) => ZUSAETZE.reduce((h, z) => h.replace(z, ''), html)
|
||||
for (const [alt, neu] of PAARE) {
|
||||
test(`${neu}: gleicher sichtbarer Text, gleiche Felder, gleiche Skripte wie ${alt}`, () => {
|
||||
// Kündigen und Widerrufen sprechen seit P7 du (Fassung 6, C14): ihr Text steht in test/knOeffentlichDu.test.js; Felder und Skripte werden weiter verglichen
|
||||
if (!['kuendigen.html', 'widerrufen.html'].includes(alt)) assert.equal(sichtbarerText(lese(neu)), sichtbarerText(lese(alt)))
|
||||
assert.deepEqual(attribute(lese(neu)), attribute(lese(alt))); assert.deepEqual(skripte(lese(neu)), skripte(lese(alt)))
|
||||
const n = ohneZusaetze(lese(neu))
|
||||
if (!['kuendigen.html', 'widerrufen.html'].includes(alt)) assert.equal(sichtbarerText(n), sichtbarerText(lese(alt)))
|
||||
assert.deepEqual(attribute(n), attribute(lese(alt))); assert.deepEqual(skripte(n), skripte(lese(alt)))
|
||||
})
|
||||
test(`${neu}: neuer Stil, hell, ohne Verläufe, Lila und Dunkelmodus`, () => {
|
||||
const n = lese(neu)
|
||||
@@ -24,6 +28,20 @@ for (const [alt, neu] of PAARE) {
|
||||
test('Login: Skript lädt app.js und startet initLoginPage, Statusleiste hell', () => {
|
||||
const n = lese('neu/oeffentlich/login.html'); assert.match(n, /<script src="\/app\.js"><\/script>/); assert.match(n, /initLoginPage\(\)/); assert.match(n, /apple-mobile-web-app-status-bar-style" content="default"/)
|
||||
})
|
||||
test('Login: Glas-Logo statt App-Icon (Handy und PC), Startbild der App als letztes im Kopf, Logo-Skript nach initLoginPage', () => {
|
||||
const n = lese('neu/oeffentlich/login.html')
|
||||
assert.match(n, /<script src="\/neu\/startbild\.js"><\/script>\s*<\/head>/)
|
||||
assert.match(n, /<div class="kn-login-marke">\s*<div class="kn-login-glas"><\/div>\s*<span>WEBklar Kundenbereich<\/span>/)
|
||||
assert.doesNotMatch(n, /app-icon-192\.png" alt=""/)
|
||||
assert.match(n, /initLoginPage\(\);\s*<\/script>\s*<script type="module" src="\/neu\/login-logo\.js"><\/script>\s*<\/body>/)
|
||||
const css = lese('neu/oeffentlich/oeffentlich.css')
|
||||
assert.doesNotMatch(css, /\.kn-login-marke\s*\{\s*display:\s*none/, 'die Marke steht jetzt auch am PC')
|
||||
assert.match(css, /\.kn-login-glas\s*\{[^}]*height:\s*220px/); assert.match(css, /\.kn-login-glas\s*\{\s*height:\s*190px;\s*\}/)
|
||||
// Der Name erscheint erst nach dem Glas, sofort wenn das Logo ruht (nach dem Startbild) oder bei reduzierter Bewegung
|
||||
assert.match(css, /\.kn-login-marke > span\s*\{[^}]*animation:\s*kn-login-name[^}]*\}/)
|
||||
assert.match(css, /\.kn-login-marke\.kn-login-ruht > span\s*\{\s*animation:\s*none;\s*\}/)
|
||||
assert.match(css, /@media \(prefers-reduced-motion: reduce\)\s*\{\s*\.kn-login-marke > span\s*\{\s*animation:\s*none;\s*\}\s*\}/)
|
||||
})
|
||||
test('Auftragsseite: Token bleibt geschützt (noindex, kein Referrer), Rückweg, Wurzel und Modul bleiben, hell, jede Klasse hat eine Regel', () => {
|
||||
const n = lese('neu/oeffentlich/auftrag.html')
|
||||
assert.match(n, /<meta name="robots" content="noindex, nofollow">/); assert.match(n, /<meta name="referrer" content="no-referrer">/); assert.match(n, /<meta name="color-scheme" content="light">/)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
import { dauer, einstellungen, frameAt, WEBKLAR } from '../public/neu/glas-logo.js'
|
||||
|
||||
const lese = (p) => readFileSync(new URL(`../public/neu/${p}`, import.meta.url), 'utf8')
|
||||
const ohneKommentare = (q) => q.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1')
|
||||
|
||||
test('Glas-Logo: WEBklar-Vorgaben (weiß, Oval #111111) und eigene Werte gehen vor', () => {
|
||||
const P = einstellungen({ finalScale: 0.9 })
|
||||
assert.equal(P.background, '#ffffff'); assert.equal(P.logoColor, '#111111'); assert.equal(P.finalScale, 0.9)
|
||||
assert.equal(P.logoScale, WEBKLAR.logoScale); assert.equal(P.present, 2.4)
|
||||
assert.equal(einstellungen().finalScale, WEBKLAR.finalScale)
|
||||
})
|
||||
|
||||
test('Glas-Logo: Zeitleiste beginnt leer, endet mit dem blanken Logo ohne Glas (3,5 s)', () => {
|
||||
const P = einstellungen({ finalScale: 0.9 })
|
||||
assert.equal(dauer(P), 3.5)
|
||||
assert.equal(frameAt(0, P).uPlateAlpha, 0)
|
||||
const mitte = frameAt(1.5, P)
|
||||
assert.equal(mitte.uPlateAlpha, 1); assert.equal(mitte.uOptics, 1); assert.equal(mitte.uBare, 0)
|
||||
const ende = frameAt(dauer(P), P)
|
||||
assert.ok(ende.uPlateAlpha < 0.01, `Glas noch sichtbar: ${ende.uPlateAlpha}`); assert.ok(ende.uOptics < 0.01); assert.equal(ende.uBare, 1)
|
||||
assert.deepEqual(ende.uBg, [1, 1, 1]); assert.deepEqual(ende.uLogoCol, [17 / 255, 17 / 255, 17 / 255])
|
||||
assert.ok(Math.abs(ende.uLogoHalf - 0.6 * 0.9 * 0.955) < 0.01, `Logogröße am Ende: ${ende.uLogoHalf}`)
|
||||
})
|
||||
|
||||
test('Glas-Logo: das Glas wird nie breiter als 1,3 Kacheln (die Anmeldung rechnet mit rand 1,4)', () => {
|
||||
const P = einstellungen({ finalScale: 0.9 })
|
||||
let max = 0
|
||||
for (let t = 0; t <= dauer(P) + 1e-9; t += 0.005) max = Math.max(max, frameAt(t, P).uPlateScale)
|
||||
assert.ok(max < 1.3, `größte Glasbreite ${max}`)
|
||||
assert.match(lese('login-logo.js'), /rand: 1\.4/)
|
||||
})
|
||||
|
||||
test('Glas-Logo: MIT-Hinweis des Originals bleibt im Quelltext', () => {
|
||||
const q = lese('glas-logo.js')
|
||||
assert.match(q, /Copyright \(c\) 2026 Zanwei Guo/); assert.match(q, /Permission is hereby granted, free of charge/)
|
||||
assert.match(q, /THE SOFTWARE IS PROVIDED "AS IS"/); assert.match(q, /github\.com\/zanwei\/glass-icon-reveal/)
|
||||
})
|
||||
|
||||
test('Glas-Logo, Anmeldung, Startbild: kein HTML-Einsetzen, nur relative Importe, kein console.log', () => {
|
||||
for (const datei of ['glas-logo.js', 'login-logo.js', 'startbild.js']) {
|
||||
const q = ohneKommentare(lese(datei))
|
||||
assert.doesNotMatch(q, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|console\.log|\beval\(/, datei)
|
||||
for (const m of q.matchAll(/from '([^']+)'/g)) assert.match(m[1], /^\.\//, `${datei}: nur relative Importe`)
|
||||
}
|
||||
// Das Startbild ist ein klassisches Skript (läuft im Kopf vor dem ersten Bild) und lädt das Modul selbst
|
||||
assert.doesNotMatch(ohneKommentare(lese('startbild.js')), /^\s*(import|export)\s/m)
|
||||
assert.match(lese('startbild.js'), /import\('\/neu\/glas-logo\.js'\)/)
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------ Startbild in einer VM --- */
|
||||
const STARTBILD = lese('startbild.js')
|
||||
|
||||
function element(tag) {
|
||||
const el = {
|
||||
tag, id: '', className: '', textContent: '', kinder: [], attrs: {}, klassen: new Set(), hoerer: {}, entfernt: false,
|
||||
setAttribute(k, v) { el.attrs[k] = v },
|
||||
appendChild(kind) { el.kinder.push(kind); return kind },
|
||||
addEventListener(typ, fn) { el.hoerer[typ] = fn },
|
||||
remove() { el.entfernt = true },
|
||||
classList: { add: (c) => el.klassen.add(c) },
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
function starte({ standalone = false, ios = false, gemerkt = false, speicherWirft = false } = {}) {
|
||||
const attr = new Map()
|
||||
const kopf = []
|
||||
const hoerer = {}
|
||||
const timer = []
|
||||
const speicher = new Map(gemerkt ? [['kn-startbild', '1']] : [])
|
||||
const wirf = () => { if (speicherWirft) throw new Error('Speicher gesperrt') }
|
||||
const document = {
|
||||
documentElement: { setAttribute: (k, v) => attr.set(k, v), removeAttribute: (k) => attr.delete(k) },
|
||||
head: { appendChild: (el) => kopf.push(el) },
|
||||
body: null,
|
||||
createElement: (tag) => element(tag),
|
||||
addEventListener: (typ, fn) => { hoerer[typ] = fn },
|
||||
}
|
||||
const window = {
|
||||
matchMedia: (q) => ({ matches: standalone && /display-mode:\s*standalone/.test(q) }),
|
||||
navigator: { standalone: ios },
|
||||
sessionStorage: { getItem: (k) => { wirf(); return speicher.get(k) ?? null }, setItem: (k, v) => { wirf(); speicher.set(k, v) } },
|
||||
}
|
||||
// import() ohne Modul-Lader schlägt in der VM fehl: wie ein Startbild, dessen Modul nicht lädt
|
||||
vm.runInNewContext(STARTBILD, { window, document, setTimeout: (fn, ms) => timer.push({ fn, ms }) })
|
||||
return { attr, kopf, hoerer, timer, speicher, document }
|
||||
}
|
||||
|
||||
const ruhe = () => new Promise((r) => setImmediate(r))
|
||||
|
||||
test('Startbild: im Browser (nicht installiert) passiert nichts', () => {
|
||||
const s = starte()
|
||||
assert.equal(s.attr.size, 0); assert.equal(s.kopf.length, 0); assert.equal(s.speicher.size, 0); assert.equal(s.hoerer.DOMContentLoaded, undefined)
|
||||
})
|
||||
|
||||
test('Startbild: nur einmal je App-Start (sessionStorage), ohne Speicher gar nicht', () => {
|
||||
for (const s of [starte({ standalone: true, gemerkt: true }), starte({ standalone: true, speicherWirft: true })]) {
|
||||
assert.equal(s.attr.size, 0); assert.equal(s.kopf.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('Startbild: installierte App deckt sofort weiß ab, zeigt die Fläche und blendet sie aus, auch wenn das Modul nicht lädt', async () => {
|
||||
for (const art of [{ standalone: true }, { ios: true }]) {
|
||||
const s = starte(art)
|
||||
assert.equal(s.attr.get('data-kn-startbild'), 'deckt'); assert.equal(s.speicher.get('kn-startbild'), '1')
|
||||
assert.equal(s.kopf.length, 1); assert.match(s.kopf[0].textContent, /html\[data-kn-startbild="deckt"\]::before\{[^}]*position:fixed[^}]*background:#fff/)
|
||||
assert.match(s.kopf[0].textContent, /#kn-startbild\.kn-startbild-weg\{opacity:0/)
|
||||
assert.equal(typeof s.hoerer.DOMContentLoaded, 'function')
|
||||
|
||||
s.document.body = element('body')
|
||||
s.hoerer.DOMContentLoaded()
|
||||
const flaeche = s.document.body.kinder[0]
|
||||
assert.equal(flaeche.id, 'kn-startbild'); assert.equal(flaeche.attrs['aria-hidden'], 'true')
|
||||
assert.equal(flaeche.kinder[0].className, 'kn-startbild-logo')
|
||||
assert.equal(s.attr.get('data-kn-startbild'), 'spielt', 'die Fläche liegt jetzt selbst über der Seite')
|
||||
assert.ok(s.timer.some((t) => t.ms === 6500), 'Sicherheitszeit')
|
||||
|
||||
await ruhe()
|
||||
const halten = s.timer.find((t) => t.ms === 120)
|
||||
assert.ok(halten, 'ohne Modul blendet die Fläche nach kurzer Pause aus')
|
||||
halten.fn()
|
||||
assert.ok(flaeche.klassen.has('kn-startbild-weg'))
|
||||
flaeche.hoerer.transitionend()
|
||||
assert.equal(flaeche.entfernt, true); assert.equal(s.attr.has('data-kn-startbild'), false)
|
||||
}
|
||||
})
|
||||
|
||||
test('Startbild: ein Tipp überspringt es', () => {
|
||||
const s = starte({ standalone: true })
|
||||
s.document.body = element('body')
|
||||
s.hoerer.DOMContentLoaded()
|
||||
const flaeche = s.document.body.kinder[0]
|
||||
flaeche.hoerer.click()
|
||||
assert.ok(flaeche.klassen.has('kn-startbild-weg'))
|
||||
})
|
||||
@@ -12,7 +12,9 @@ test('dashboard.html: weiße PWA-Metas, Stile in fester Reihenfolge, klassisches
|
||||
const css = [...html.matchAll(/<link rel="stylesheet" href="([^"]+)"/g)].map((m) => m[1]); const ORDNUNG = ['/neu/tokens.css', '/neu/basis.css', '/neu/huelle.css', '/neu/ansichten.css', '/neu/funktionen.css', '/neu/module.css', '/neu/wasser-leiste.css'] // Wasser-Leiste zuletzt: überschreibt die Handy-Zeile aus huelle.css
|
||||
assert.deepEqual(css.filter((f) => f !== '/neu/chat-composer.css'), ORDNUNG); assert.ok(css.indexOf('/neu/chat-composer.css') === -1 || css.indexOf('/neu/chat-composer.css') > css.indexOf('/neu/huelle.css'))
|
||||
assert.match(html, /<body class="kn">/)
|
||||
const skripte = [...html.matchAll(/<script([^>]*)>/g)].map((m) => m[1].trim())
|
||||
// Startbild der installierten App: klassisches Skript als letztes im Kopf, damit es vor dem ersten Bild läuft
|
||||
assert.match(html, /<script src="\/neu\/startbild\.js"><\/script>\s*<\/head>/)
|
||||
const skripte = [...html.slice(html.indexOf('<body')).matchAll(/<script([^>]*)>/g)].map((m) => m[1].trim())
|
||||
assert.deepEqual([skripte[0], skripte.at(-1)], ['src="/kundenansicht.js"', 'type="module" src="/neu/huelle.js"']); assert.ok(skripte.slice(1, -1).every((s) => s === 'src="/chat-composer.js"'))
|
||||
assert.doesNotMatch(html, /login\.css|kundenbereich\.css|\/app\.js|dot-background|kb-glow|class="[^"]*\bkb-/)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user