Bei der Systemabnahme gemessen und fuer untragbar befunden: Die Verwaltung lief mit 4,5 Bildern je Sekunde, sobald die Maus bewegt wurde. Die Startseite lag bei 60. Ein Arbeitswerkzeug, das bei jeder Mausbewegung ruckelt, ist kein Gewinn an Schoenheit. WAS GEMESSEN WURDE, STATT GERATEN Jeder Effekt einzeln abgeschaltet, mit echter Mausbewegung: alles an 6,4 Bilder/s ohne Glas 9,7 ohne Lampe 9,2 ohne Kachel-Neigung 6,4 (kostet NICHTS) ohne Glas UND Lampe 16,4 Kein einzelner Schuldiger -- es war die Wechselwirkung. Die Lampe verschiebt den Untergrund, woraufhin JEDE Glasflaeche darueber ihre Rueckseiten-Unschaerfe neu berechnen muss. Beide zusammen kosteten mehr als beide einzeln. Die Neigung ist gratis: Sie laeuft auf der Grafikkarte. Ein zweiter Posten kam dazu: Solange die Kacheln durchscheinend sind, muss das bildschirmfuellende Buehnenbild bei jeder Neuzeichnung mitgerechnet werden. Ohne Buehnenbild stieg die Rate von 24 auf 34,6. DREI EINGRIFFE 1. Glas nur noch auf dem Detailblatt. Davon gibt es immer genau eines, und es liegt gross ueber der Seite -- dort faellt die Rechenzeit einmal an, nicht pro Listeneintrag. Kacheln, Reiter und Knoepfe bekommen stattdessen eine dichte Flaeche. Optisch kaum ein Unterschied, weil die Struktur des Bildes ohnehin verschwindet. 2. Die Vollbild-Zeigerlampe ist abgeschaltet. Das Licht, das dem Zeiger folgt, gibt es weiterhin -- auf den Kacheln selbst. Das ist der Effekt, der zaehlt, und er ist billig: Er betrifft nur die Kachel unter dem Zeiger statt des ganzen Bildschirms. 3. Die Kachelflaeche ist dichter (.92/.96 statt .52/.68). Die Buehne bleibt rings um die Kacheln voll sichtbar, durch die Kachel selbst schimmert sie nur noch als Ahnung. ERGEBNIS in Ruhe (lesen) 55,6 -> 60,6 Bilder/s beim Scrollen 54,0 Zeiger bewegt 4,5 -> 28 DIE BILDRATE IST JETZT SELBST EIN PRUEFPUNKT Ohne ihn kaeme jederzeit ein weiterer huebscher Effekt dazu, der die Seite still wieder zaeh macht -- und niemand wuesste, welcher es war. Der Durchlauf verlangt jetzt ueber 45 Bilder je Sekunde in Ruhe. NEBENBEFUND Die Regel fuer das Detailblatt stand unter "#vw-bereich" -- das Blatt liegt aber in einer eigenen Ueberlagerung. Die Regel griff also nie: Ausgerechnet die eine Flaeche, die eine Unschaerfe wirklich verdient, hatte als einzige keine. Gefunden hat das der neue Test. Geprueft: 1281 Pruefungen gruen (Browser 715, Server 566), i18n ohne Fehler, WCAG ohne Fundstellen. Co-Authored-By: Claude Opus 5 <[email protected]>
707 lines
32 KiB
JavaScript
707 lines
32 KiB
JavaScript
/* =====================================================================
|
|
Prueft die Buehne im Verwaltungsbereich.
|
|
|
|
Das Konzept hat sich umgedreht, und damit auch die Pruefung.
|
|
|
|
Vorher: ein schmaler Streifen, auf 190 % gezoomt, davon ein Ausschnitt
|
|
gewaehlt und die linke Haelfte voll zugedeckt -- damit Text darauf
|
|
lesbar bleibt. Gemessen wurde, ob das Bild leise genug ist. Uebrig
|
|
blieb ein Zipfel.
|
|
|
|
Jetzt: Das Motiv ist die Buehne -- bildschirmfuellend, ungeschnitten,
|
|
ungedimmt. Der Inhalt schwebt als Milchglas darueber. Text steht nie
|
|
direkt auf dem Bild.
|
|
|
|
Deshalb wird jetzt das Gegenteil geprueft:
|
|
1. Ist das Bild wirklich GANZ da? (kein Zoom, kein Ausschnitt)
|
|
2. Traegt jede Flaeche, auf der gelesen wird, auch Glas?
|
|
3. Bleibt der Text darauf lesbar -- an echten Bildpunkten gemessen?
|
|
===================================================================== */
|
|
import { chromium } from "playwright";
|
|
import http from "http";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const TYP = {
|
|
".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
|
".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
|
|
".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
|
|
};
|
|
|
|
const fehlt = [];
|
|
const srv = http.createServer((q, r) => {
|
|
const u = decodeURIComponent(q.url.split("?")[0]);
|
|
const p = path.join(process.cwd(), u);
|
|
if (fs.existsSync(p) && !p.endsWith("/")) {
|
|
r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
|
|
return r.end(fs.readFileSync(p));
|
|
}
|
|
fehlt.push(u);
|
|
r.writeHead(404);
|
|
r.end("");
|
|
});
|
|
|
|
let ok = 0, fehl = 0;
|
|
const pruefe = (name, gut, info) => {
|
|
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
|
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
|
|
};
|
|
|
|
function leuchtkraft([r, g, b]) {
|
|
const f = (v) => {
|
|
v /= 255;
|
|
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
};
|
|
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
}
|
|
function verhaeltnis(a, b) {
|
|
const l1 = leuchtkraft(a), l2 = leuchtkraft(b);
|
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
}
|
|
|
|
const BEREICHE = ["uebersicht", "anfragen", "projekte", "kunden", "zahlungen", "postfach"];
|
|
|
|
/* Setzt einen Bereich und legt Beispielinhalt ein. Ohne Inhalt liesse
|
|
sich nicht messen, ob Text ueber der Buehne lesbar bleibt -- und genau
|
|
das ist die Frage, an der das ganze Konzept haengt.
|
|
|
|
Der Aufbau passiert vor JEDER Messung neu: Der Datenabruf laeuft im
|
|
Hintergrund weiter und klappt den Arbeitsbereich verzoegert wieder zu
|
|
(Zugangspruefung, kein Fehler). */
|
|
async function bereichAufbauen(seite, bereich) {
|
|
await seite.evaluate((b) => {
|
|
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
|
|
const bereichEl = document.querySelector("#vw-bereich");
|
|
bereichEl.removeAttribute("hidden");
|
|
document.documentElement.setAttribute("data-vw-bereich", b);
|
|
document.querySelectorAll("[data-tab]").forEach((x) =>
|
|
x.classList.toggle("vw-an", x.dataset.tab === b));
|
|
|
|
let probe = document.querySelector("#vw-probe");
|
|
if (!probe) {
|
|
probe = document.createElement("div");
|
|
probe.id = "vw-probe";
|
|
/* DIE ECHTE KACHELSTRUKTUR der Verwaltung, so wie das Skript sie
|
|
erzeugt: .vw-karte mit Nummer, Mitte und rechter Spalte.
|
|
|
|
Frueher stand hier eine selbst gebaute .wd-karte. Die kommt im
|
|
Verwaltungsbereich aber fast nicht vor -- der Test hat also
|
|
eine Attrappe geprueft und war gruen, waehrend auf den echten
|
|
Kacheln nichts davon ankam. Genau deshalb war der Unterschied
|
|
nirgends zu sehen. */
|
|
probe.innerHTML =
|
|
'<button class="vw-karte">' +
|
|
'<span class="vw-karte-nr">P-2608-0002</span>' +
|
|
'<span class="vw-karte-mitte">' +
|
|
'<span class="vw-karte-name">Onepager fuer Vanessa</span>' +
|
|
'<span class="vw-karte-info" id="vw-probe-text">Vanessa Geske</span>' +
|
|
'</span>' +
|
|
'<span class="vw-karte-rechts"><span class="vw-mini">Design</span></span>' +
|
|
'</button>' +
|
|
'<button class="vw-karte">' +
|
|
'<span class="vw-karte-nr">P-2608-0004</span>' +
|
|
'<span class="vw-karte-mitte">' +
|
|
'<span class="vw-karte-name">Betreuung</span>' +
|
|
'<span class="vw-karte-info">Marc Weber</span>' +
|
|
'</span>' +
|
|
'<span class="vw-karte-rechts"><span class="vw-mini vw-mini--geld">39,00</span></span>' +
|
|
'</button>';
|
|
bereichEl.appendChild(probe);
|
|
}
|
|
}, bereich);
|
|
await seite.waitForTimeout(700);
|
|
}
|
|
|
|
/* Misst die Schwankung des Untergrunds an echten Bildpunkten. Hohe
|
|
Schwankung heisst: Da liegt Bildstruktur unter der Schrift. */
|
|
async function unruheAn(seite, wahl) {
|
|
const bild = await seite.locator(wahl).first().screenshot();
|
|
return seite.evaluate(async (daten) => {
|
|
const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob());
|
|
const bm = await createImageBitmap(bl);
|
|
const c = new OffscreenCanvas(bm.width, bm.height);
|
|
const x = c.getContext("2d");
|
|
x.drawImage(bm, 0, 0);
|
|
const d = x.getImageData(0, 0, bm.width, bm.height).data;
|
|
const grund = [];
|
|
for (let i = 0; i < d.length; i += 4) {
|
|
const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
if (h < 120) grund.push(h);
|
|
}
|
|
if (grund.length < 50) return null;
|
|
const m = grund.reduce((a, b) => a + b, 0) / grund.length;
|
|
return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length));
|
|
}, bild.toString("base64"));
|
|
}
|
|
|
|
srv.listen(4404, async () => {
|
|
const browser = await chromium.launch();
|
|
const seite = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
const skriptfehler = [];
|
|
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
|
|
await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
|
|
await seite.waitForTimeout(1500);
|
|
|
|
console.log("===== 1. IST DAS BILD GANZ DA? =====");
|
|
await bereichAufbauen(seite, "kunden");
|
|
const buehne = await seite.evaluate(() => {
|
|
const el = document.querySelector(".vw-buehne");
|
|
if (!el) return null;
|
|
const st = getComputedStyle(el);
|
|
const r = el.getBoundingClientRect();
|
|
/* Seit Buehne und Lampe in einem gemeinsamen Raum stecken, sitzen
|
|
Haltung, Stapelplatz und die Auszeichnung als Zierde am RAUM --
|
|
die Buehne darin ist absolut positioniert. Geprueft wird deshalb
|
|
der Raum, sonst misst der Test am falschen Element. */
|
|
const raum = el.parentElement;
|
|
const rs = getComputedStyle(raum);
|
|
const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
|
|
return {
|
|
datei: treffer ? treffer[1] : "(keins)",
|
|
groesse: st.backgroundSize,
|
|
lage: st.backgroundPosition,
|
|
haltung: st.position,
|
|
breite: Math.round(r.width),
|
|
hoehe: Math.round(r.height),
|
|
haltungRaum: rs.position,
|
|
z: parseInt(rs.zIndex, 10),
|
|
klicks: rs.pointerEvents,
|
|
versteckt: raum.getAttribute("aria-hidden"),
|
|
};
|
|
});
|
|
if (!buehne) { pruefe("Buehne vorhanden", false); }
|
|
else {
|
|
/* Das ist der Kern der Korrektur. Beim ersten Anlauf stand hier
|
|
"190% auto" plus ein Ausschnitt wie "84% 46%" -- damit war der
|
|
groesste Teil des Bildes weggeschnitten. */
|
|
pruefe("das Bild wird NICHT gezoomt", buehne.groesse === "cover", buehne.groesse);
|
|
pruefe("und NICHT einseitig ausgeschnitten",
|
|
/50%\s+50%|center/.test(buehne.lage), buehne.lage);
|
|
pruefe("die Buehne fuellt den ganzen Bildschirm",
|
|
buehne.breite >= 1430 && buehne.hoehe >= 890, `${buehne.breite}x${buehne.hoehe}`);
|
|
pruefe("sie steht fest, der Inhalt scrollt darueber",
|
|
buehne.haltungRaum === "fixed", buehne.haltungRaum);
|
|
pruefe("sie liegt hinter allem", buehne.z < 0, "z-index " + buehne.z);
|
|
pruefe("sie faengt keine Klicks ab", buehne.klicks === "none", buehne.klicks);
|
|
pruefe("sie ist als Zierde ausgezeichnet", buehne.versteckt === "true");
|
|
}
|
|
|
|
console.log("\n===== 2. BLEIBT DER TEXT VOM BILD GETRENNT? =====");
|
|
/* Frueher stand hier "traegt alles Glas?". Das war zu teuer: Eine
|
|
Rueckseiten-Unschaerfe kostet Rechenzeit PRO ELEMENT, und zwar bei
|
|
jeder Bewegung neu. Mit sechs Kacheln fiel die Bildrate auf 6,4
|
|
Bilder je Sekunde, bei echter Mausbewegung sogar auf 4,5.
|
|
|
|
Entscheidend ist nicht die Unschaerfe, sondern dass Text nicht auf
|
|
dem Bild steht. Das leistet auch eine dichte Flaeche -- und die
|
|
kostet nichts. */
|
|
const flaeche = await seite.evaluate(() => {
|
|
const el = document.querySelector("#vw-probe .vw-karte");
|
|
if (!el) return null;
|
|
return getComputedStyle(el).backgroundImage;
|
|
});
|
|
pruefe("die Kachel hat eine eigene Flaeche unter dem Text",
|
|
flaeche && /gradient/.test(flaeche), flaeche ? "Verlauf vorhanden" : "keine Kachel");
|
|
|
|
/* Das Detailblatt behaelt die Unschaerfe: Davon gibt es immer nur
|
|
genau eines, und es liegt gross ueber der Seite -- dort faellt die
|
|
Rechenzeit einmal an, nicht pro Listeneintrag. */
|
|
const blatt = await seite.evaluate(() => {
|
|
const el = document.querySelector(".vw-detail-blatt");
|
|
return el ? (getComputedStyle(el).backdropFilter || "keins") : "(nicht auf der Seite)";
|
|
});
|
|
if (blatt !== "(nicht auf der Seite)") {
|
|
pruefe("das Detailblatt behaelt seine Unschaerfe", /blur/.test(blatt), blatt.slice(0, 30));
|
|
}
|
|
|
|
/* Die Bildrate ist selbst ein Pruefpunkt. Ohne ihn kaeme jederzeit
|
|
ein weiterer huebscher Effekt dazu, der die Seite still wieder zaeh
|
|
macht -- und niemand wuesste, welcher es war. */
|
|
const bildrate = await seite.evaluate(() => new Promise((fertig) => {
|
|
let n = 0;
|
|
const start = performance.now();
|
|
const zaehl = () => {
|
|
n++;
|
|
if (performance.now() - start < 1200) requestAnimationFrame(zaehl);
|
|
else fertig(n / ((performance.now() - start) / 1000));
|
|
};
|
|
requestAnimationFrame(zaehl);
|
|
}));
|
|
pruefe("die Seite laeuft in Ruhe fluessig (ueber 45 Bilder/s)",
|
|
bildrate > 45, bildrate.toFixed(1) + " Bilder/s");
|
|
|
|
console.log("\n===== 3. BLEIBT DER TEXT LESBAR? =====");
|
|
for (const bereich of BEREICHE) {
|
|
await bereichAufbauen(seite, bereich);
|
|
|
|
const aufGlas = await unruheAn(seite, "#vw-probe .vw-karte");
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".vw-buehne").style.visibility = "hidden";
|
|
});
|
|
await seite.waitForTimeout(350);
|
|
await bereichAufbauen(seite, bereich);
|
|
const ohneBuehne = await unruheAn(seite, "#vw-probe .vw-karte");
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".vw-buehne").style.visibility = "";
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
|
|
const mehr = (aufGlas ?? 99) - (ohneBuehne ?? 0);
|
|
/* Etwas Struktur DARF durchkommen -- das ist ja der Reiz. Sie darf
|
|
nur nicht so stark werden, dass die Schrift darauf flimmert. */
|
|
pruefe(` ${bereich}: der Kachelgrund bleibt ruhig`,
|
|
aufGlas !== null && ohneBuehne !== null && mehr <= 8,
|
|
`mit Buehne ${aufGlas}, ohne ${ohneBuehne} → plus ${mehr}`);
|
|
|
|
await bereichAufbauen(seite, bereich);
|
|
/* Der tatsaechliche Kachelgrund wird aus dem BILD gelesen, nicht aus
|
|
dem Stilblatt: Die Kachel ist halbdurchsichtig, ihr rechnerischer
|
|
Wert sagt nichts darueber, was am Ende darunter liegt. */
|
|
const stelle = await seite.evaluate(() => {
|
|
const k = document.querySelector("#vw-probe .vw-karte");
|
|
const r = k.getBoundingClientRect();
|
|
return { x: Math.round(r.x + r.width * 0.72), y: Math.round(r.y + r.height * 0.62) };
|
|
});
|
|
const punkt = await seite.screenshot({
|
|
clip: { x: stelle.x, y: stelle.y, width: 10, height: 10 } });
|
|
const grundfarbe = await seite.evaluate(async (daten) => {
|
|
const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob());
|
|
const bm = await createImageBitmap(bl);
|
|
const c = new OffscreenCanvas(bm.width, bm.height);
|
|
const x = c.getContext("2d");
|
|
x.drawImage(bm, 0, 0);
|
|
const d = x.getImageData(0, 0, bm.width, bm.height).data;
|
|
let r = 0, g = 0, b = 0, n = 0;
|
|
for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2]; n++; }
|
|
return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
|
|
}, punkt.toString("base64"));
|
|
|
|
const schrift = await seite.evaluate(() =>
|
|
getComputedStyle(document.querySelector("#vw-probe-text"))
|
|
.color.match(/[\d.]+/g).slice(0, 3).map(Number));
|
|
|
|
const v = verhaeltnis(schrift, grundfarbe);
|
|
pruefe(` ${bereich}: Text auf der Kachel erfuellt AA (4,5:1)`,
|
|
v >= 4.5, v.toFixed(2) + ":1");
|
|
}
|
|
|
|
console.log("\n===== 3b. FOLGT DAS LICHT DER MAUS? =====");
|
|
await bereichAufbauen(seite, "kunden");
|
|
const karte = seite.locator("#vw-probe .vw-karte").first();
|
|
const kasten = await karte.boundingBox();
|
|
|
|
/* WICHTIG: mit steps bewegen. Ein Sprung ohne Zwischenschritte
|
|
erzeugt kein pointermove -- der Test wuerde "Licht folgt nicht"
|
|
melden, obwohl alles stimmt. Diese Falle hat hier schon einmal
|
|
einen Fehlalarm ausgeloest. */
|
|
await seite.mouse.move(kasten.x + 30, kasten.y + 20, { steps: 8 });
|
|
await seite.waitForTimeout(320);
|
|
const lichtA = await karte.evaluate((e) => ({
|
|
x: getComputedStyle(e).getPropertyValue("--wd-lichtx").trim(),
|
|
y: getComputedStyle(e).getPropertyValue("--wd-lichty").trim(),
|
|
}));
|
|
await seite.mouse.move(kasten.x + kasten.width - 40,
|
|
kasten.y + kasten.height - 20, { steps: 16 });
|
|
await seite.waitForTimeout(400);
|
|
const lichtB = await karte.evaluate((e) => {
|
|
const st = getComputedStyle(e);
|
|
const na = getComputedStyle(e, "::after");
|
|
return {
|
|
x: st.getPropertyValue("--wd-lichtx").trim(),
|
|
y: st.getPropertyValue("--wd-lichty").trim(),
|
|
lumen: st.getPropertyValue("--wd-lumen").trim(),
|
|
kante: st.backgroundImage.includes("radial-gradient"),
|
|
kegel: na.backgroundImage,
|
|
};
|
|
});
|
|
pruefe("das Licht wandert mit dem Zeiger",
|
|
lichtA.x !== lichtB.x && lichtA.y !== lichtB.y,
|
|
lichtA.x + "/" + lichtA.y + " -> " + lichtB.x + "/" + lichtB.y);
|
|
pruefe("die Kachelkante leuchtet an der Zeigerstelle", lichtB.kante);
|
|
/* Der Lichtkegel muss die Leitfarbe des Bereichs tragen, nicht die
|
|
Grundfarbe -- sonst leuchtet im Kundenbereich Baby Blue statt
|
|
Indigo. */
|
|
pruefe("der Lichtkegel traegt die Leitfarbe",
|
|
/97,\s*92,\s*255/.test(lichtB.kegel), lichtB.lumen);
|
|
|
|
console.log("\n===== 3c. BEWEGT SICH DIE BUEHNE MIT? =====");
|
|
await seite.mouse.move(160, 780, { steps: 12 });
|
|
await seite.waitForTimeout(650);
|
|
const lage1 = await seite.evaluate(() =>
|
|
getComputedStyle(document.querySelector(".vw-buehne")).translate);
|
|
await seite.mouse.move(1320, 130, { steps: 20 });
|
|
await seite.waitForTimeout(750);
|
|
const lage2 = await seite.evaluate(() =>
|
|
getComputedStyle(document.querySelector(".vw-buehne")).translate);
|
|
pruefe("die Buehne verschiebt sich gegen den Zeiger",
|
|
lage1 !== lage2 && lage1 !== "none", lage1 + " -> " + lage2);
|
|
|
|
/* Der Ausschlag muss klein bleiben. Waere er gross, wuerde beim Lesen
|
|
staendig etwas im Augenwinkel wandern. */
|
|
const weite = Math.max(...String(lage2).match(/-?[\d.]+/g).map((n) => Math.abs(+n)));
|
|
pruefe("und zwar nur wenige Bildpunkte", weite <= 16, weite.toFixed(1) + "px");
|
|
|
|
console.log("\n===== 3d. DER LEUCHTBALKEN =====");
|
|
/* Der Balken muss GENAU am aktiven Reiter sitzen. Gemessen wird
|
|
gegen die echten Masse des Reiters -- feste Werte waeren hier
|
|
zwangslaeufig falsch, weil die Reiter unterschiedlich breit sind
|
|
und beim Sprachwechsel wandern. */
|
|
const balkenPruefen = async (bereich) => {
|
|
/* Erst den Bereich sichtbar machen, DANN klicken: Ein Klick auf
|
|
einen zugeklappten Arbeitsbereich misst zwangslaeufig 0.
|
|
Danach wieder aufmachen -- der Klick loest die Zugangspruefung
|
|
aus, die korrekterweise zur Anmeldung zurueckschaltet. */
|
|
await bereichAufbauen(seite, bereich);
|
|
await seite.click(`[data-tab="${bereich}"]`).catch(() => {});
|
|
await seite.waitForTimeout(500);
|
|
await bereichAufbauen(seite, bereich);
|
|
await seite.evaluate(() => window.vwBalkenSetzen && window.vwBalkenSetzen());
|
|
await seite.waitForTimeout(400);
|
|
return seite.evaluate(() => {
|
|
const l = document.querySelector("#vw-bereich .vw-tabs");
|
|
const a = l.querySelector(".vw-filter-knopf.vw-an");
|
|
const lr = l.getBoundingClientRect();
|
|
const ar = a.getBoundingClientRect();
|
|
const st = l.style;
|
|
return {
|
|
gesetztX: parseInt(st.getPropertyValue("--vw-balken-x"), 10),
|
|
gesetztB: parseInt(st.getPropertyValue("--vw-balken-b"), 10),
|
|
echtX: Math.round(ar.left - lr.left),
|
|
echtB: Math.round(ar.width),
|
|
sichtbar: st.getPropertyValue("--vw-balken-da"),
|
|
reiter: a.textContent.trim().slice(0, 12),
|
|
};
|
|
});
|
|
};
|
|
|
|
const b1 = await balkenPruefen("uebersicht");
|
|
pruefe("der Balken sitzt am aktiven Reiter",
|
|
Math.abs(b1.gesetztX - b1.echtX) <= 2 && Math.abs(b1.gesetztB - b1.echtB) <= 2,
|
|
`${b1.reiter}: gesetzt ${b1.gesetztX}/${b1.gesetztB}, echt ${b1.echtX}/${b1.echtB}`);
|
|
pruefe("und er ist sichtbar", b1.sichtbar === "1", b1.sichtbar);
|
|
/* Der haeufigste Fehlerfall: Beim Start ist der Arbeitsbereich noch
|
|
versteckt, die Leiste also 0 Punkte breit. Wer nur auf "resize"
|
|
horcht, misst nie nach -- der Balken bleibt auf Breite 0. */
|
|
pruefe("er hat eine echte Breite (nicht 0)", b1.gesetztB > 20, b1.gesetztB + "px");
|
|
|
|
const b2 = await balkenPruefen("zahlungen");
|
|
pruefe("er wandert zum neuen Reiter mit",
|
|
b2.gesetztX !== b1.gesetztX && Math.abs(b2.gesetztX - b2.echtX) <= 2,
|
|
`${b1.gesetztX}px -> ${b2.gesetztX}px`);
|
|
|
|
console.log("\n===== 3e. TAUCHEN DIE KACHELN GESTAFFELT AUF? =====");
|
|
const staffel = await seite.evaluate(() => {
|
|
const be = document.querySelector("#vw-bereich");
|
|
/* EIGENER Behaelter. Frueher wurde hier die Probekachel geleert --
|
|
danach fehlten ihr Ueberschrift und Text, und der Neigungstest
|
|
weiter unten stuerzte ab, weil er auf ein nicht vorhandenes
|
|
Element zugriff. */
|
|
let probe = document.querySelector("#vw-staffel");
|
|
if (!probe) {
|
|
probe = document.createElement("div");
|
|
probe.id = "vw-staffel";
|
|
be.appendChild(probe);
|
|
}
|
|
probe.innerHTML = "";
|
|
for (let i = 0; i < 4; i++) {
|
|
const k = document.createElement("button");
|
|
k.className = "vw-karte";
|
|
k.style.padding = "1rem";
|
|
k.textContent = "Kachel " + (i + 1);
|
|
probe.appendChild(k);
|
|
}
|
|
return [...probe.querySelectorAll(".wd-karte")].map((k) => ({
|
|
name: getComputedStyle(k).animationName,
|
|
versatz: getComputedStyle(k).animationDelay,
|
|
}));
|
|
});
|
|
pruefe("die Kacheln haben eine Auftauch-Animation",
|
|
staffel.every((k) => k.name === "vw-kachel-auf"),
|
|
staffel.map((k) => k.name).join(", "));
|
|
pruefe("und jede startet etwas spaeter als die davor",
|
|
new Set(staffel.map((k) => k.versatz)).size === staffel.length,
|
|
staffel.map((k) => k.versatz).join(" "));
|
|
|
|
console.log("\n===== 3f. ZEIGERLAMPE, PRISMA UND PARTIKEL =====");
|
|
await bereichAufbauen(seite, "anfragen");
|
|
await seite.mouse.move(300, 300, { steps: 8 });
|
|
await seite.waitForTimeout(300);
|
|
await seite.mouse.move(1050, 380, { steps: 20 });
|
|
await seite.waitForTimeout(700);
|
|
|
|
const extra = await seite.evaluate(() => {
|
|
const raum = document.querySelector(".vw-raum");
|
|
const lampe = document.querySelector(".vw-lampe");
|
|
const flocken = document.querySelector(".vw-flocken");
|
|
const karte = document.querySelector("#vw-probe .vw-karte");
|
|
const ls = getComputedStyle(lampe);
|
|
return {
|
|
raumIsoliert: getComputedStyle(raum).isolation,
|
|
mischung: ls.mixBlendMode,
|
|
lampeAn: lampe.classList.contains("vw-lampe-an"),
|
|
mx: lampe.style.getPropertyValue("--vw-mx"),
|
|
my: lampe.style.getPropertyValue("--vw-my"),
|
|
flockenZahl: flocken ? flocken.children.length : 0,
|
|
flockeLaeuft: flocken && flocken.children[0]
|
|
? getComputedStyle(flocken.children[0]).animationName : "",
|
|
kachelKante: karte ? getComputedStyle(karte).backgroundImage : "",
|
|
};
|
|
});
|
|
|
|
/* Die Isolation ist der Kern. Ohne sie mischt sich die Lampe mit
|
|
ALLEM in ihrem Stapelkontext -- auch mit Kacheln, die eigentlich
|
|
darueber liegen. Beim Bauen lag dadurch ein gruenlicher Fleck
|
|
mitten auf einer Karte. */
|
|
pruefe("Buehne und Lampe stecken in einem isolierten Raum",
|
|
extra.raumIsoliert === "isolate", extra.raumIsoliert);
|
|
|
|
/* Die Vollbild-Zeigerlampe ist ABGESCHALTET -- eine Entscheidung,
|
|
kein Versehen. Zusammen mit den Glasflaechen kostete sie zwei
|
|
Drittel der Bildrate: Jede ihrer Bewegungen zwang jede Flaeche
|
|
darueber zum Neurechnen.
|
|
|
|
Das Licht, das dem Zeiger folgt, gibt es weiterhin auf den Kacheln
|
|
selbst. Es ist billig, weil es nur die Kachel unter dem Zeiger
|
|
betrifft statt des ganzen Bildschirms. */
|
|
const lampeAus = await seite.evaluate(() =>
|
|
getComputedStyle(document.querySelector(".vw-lampe")).display);
|
|
pruefe("die Vollbild-Lampe ist bewusst aus", lampeAus === "none", lampeAus);
|
|
/* soft-light rechnet mit dem, was darunter liegt: dunkle Stellen
|
|
bleiben dunkel, vorhandene Lichtkanten werden verstaerkt. "screen"
|
|
oder "overlay" wuerden die Eiskanten ausbrennen lassen. */
|
|
|
|
|
|
/* Erst zeigen, wenn der Zeiger wirklich da war -- sonst haengt beim
|
|
Laden ein Lichtfleck in der Bildmitte, den niemand dorthin gelegt
|
|
hat. */
|
|
|
|
|
|
pruefe("es schweben Partikel im Raum", extra.flockenZahl === 9,
|
|
extra.flockenZahl + " Stueck");
|
|
pruefe("und sie bewegen sich", extra.flockeLaeuft === "vw-schweben",
|
|
extra.flockeLaeuft);
|
|
|
|
/* Der Prisma-Schimmer faechert die Kante in Nachbartoene auf. Geprueft
|
|
wird, dass mehr als nur die Leitfarbe drinsteckt -- sonst waere es
|
|
wieder ein einfarbiger Rand. */
|
|
const toene = (extra.kachelKante.match(/rgba?\(/g) || []).length;
|
|
pruefe("die Kachelkante bricht das Licht in mehrere Toene", toene >= 6,
|
|
toene + " Farbstufen");
|
|
|
|
console.log("\n===== 3g. NEIGEN SICH DIE KACHELN? =====");
|
|
await bereichAufbauen(seite, "kunden");
|
|
const platte = seite.locator("#vw-probe .vw-karte").first();
|
|
const pk = await platte.boundingBox();
|
|
|
|
/* Mit steps bewegen, sonst entsteht kein pointermove. */
|
|
await seite.mouse.move(pk.x + 25, pk.y + 20, { steps: 10 });
|
|
await seite.waitForTimeout(420);
|
|
const eckeA = await platte.evaluate((e) => ({
|
|
nx: parseFloat(e.style.getPropertyValue("--vw-nx")),
|
|
ny: parseFloat(e.style.getPropertyValue("--vw-ny")),
|
|
matrix: getComputedStyle(e).transform,
|
|
}));
|
|
await seite.mouse.move(pk.x + pk.width - 25, pk.y + pk.height - 20, { steps: 18 });
|
|
await seite.waitForTimeout(420);
|
|
const eckeB = await platte.evaluate((e) => ({
|
|
nx: parseFloat(e.style.getPropertyValue("--vw-nx")),
|
|
ny: parseFloat(e.style.getPropertyValue("--vw-ny")),
|
|
matrix: getComputedStyle(e).transform,
|
|
}));
|
|
|
|
/* Die Werte muessen unabhaengig von der Kachelgroesse sein: -1 am
|
|
linken Rand, +1 am rechten. Sonst kippt eine breite Kachel
|
|
staerker als eine schmale, obwohl der Zeiger relativ gleich steht. */
|
|
/* Geprueft wird der VORZEICHENWECHSEL, nicht ein fester Betrag.
|
|
Eine flache Kachel ist nur gut 100 Punkte hoch -- 20 Punkte vom
|
|
Rand liegen dort schon fast in der Mitte, der senkrechte Wert
|
|
bleibt also klein. Ein Schwellwert von 0,5 haette nur gemessen,
|
|
wie hoch die Testkachel zufaellig ist. */
|
|
pruefe("die Neigung kippt in die Gegenrichtung",
|
|
eckeA.nx < -0.4 && eckeB.nx > 0.4 &&
|
|
eckeA.ny < 0 && eckeB.ny > 0,
|
|
`${eckeA.nx}/${eckeA.ny} -> ${eckeB.nx}/${eckeB.ny}`);
|
|
pruefe("und wird als echte Raumdrehung umgesetzt",
|
|
eckeA.matrix.startsWith("matrix3d") && eckeA.matrix !== eckeB.matrix,
|
|
eckeA.matrix.slice(0, 26) + "...");
|
|
|
|
/* Der Ausschlag muss klein bleiben, sonst verzerrt die Schrift. Aus
|
|
der Matrix laesst sich der Winkel ueber den Eintrag [0][2]
|
|
zurueckrechnen: sin(Drehung um die Hochachse). */
|
|
/* NUR den Inhalt der Klammern auswerten. Ein Suchmuster ueber die
|
|
ganze Zeichenkette liest die "3" aus "matrix3d" als erste Zahl mit
|
|
und verschiebt damit jeden Eintrag um eine Stelle -- der Winkel kam
|
|
dadurch als 0,4 Grad heraus statt als 7. */
|
|
const inhalt = eckeB.matrix.slice(eckeB.matrix.indexOf("(") + 1, -1);
|
|
const zahlen = inhalt.split(",").map((n) => parseFloat(n));
|
|
const winkel = Math.abs(Math.asin(Math.max(-1, Math.min(1, zahlen[2]))) * 180 / Math.PI);
|
|
pruefe("aber flach genug, dass die Schrift scharf bleibt", winkel <= 9,
|
|
winkel.toFixed(1) + " Grad");
|
|
|
|
/* Beim Verlassen zuruecksetzen -- sonst bleibt die Kachel schraeg
|
|
stehen, nachdem der Zeiger laengst weg ist. */
|
|
await seite.mouse.move(40, 40, { steps: 14 });
|
|
await seite.waitForTimeout(520);
|
|
const zurueck = await platte.evaluate((e) => ({
|
|
nx: e.style.getPropertyValue("--vw-nx"),
|
|
ny: e.style.getPropertyValue("--vw-ny"),
|
|
}));
|
|
pruefe("und sie stellt sich beim Verlassen zurueck",
|
|
zurueck.nx === "0" && zurueck.ny === "0", `${zurueck.nx}/${zurueck.ny}`);
|
|
|
|
const tiefe = await seite.evaluate(() => {
|
|
const k = document.querySelector("#vw-probe .vw-karte");
|
|
const h = k.querySelector(".vw-karte-nr");
|
|
const p = k.querySelector(".vw-karte-mitte");
|
|
return {
|
|
ueberschrift: getComputedStyle(h).transform,
|
|
absatz: getComputedStyle(p).transform,
|
|
ecken: getComputedStyle(k).borderRadius,
|
|
glanz: getComputedStyle(k, "::before").backgroundImage.slice(0, 30),
|
|
};
|
|
});
|
|
/* Der Inhalt steht vor der Flaeche, Ueberschriften weiter vorn als
|
|
Fliesstext -- dadurch entsteht beim Neigen echte Staffelung. */
|
|
pruefe("der Inhalt steht vor der Flaeche",
|
|
tiefe.ueberschrift !== "none" && tiefe.absatz !== "none" &&
|
|
tiefe.ueberschrift !== tiefe.absatz,
|
|
"Ueberschrift und Text auf verschiedenen Ebenen");
|
|
/* Ungleiche Ecken: Eine gleichmaessig gerundete Kachel liest sich als
|
|
Knopf, die ungleiche nimmt die Facetten der Motive auf. */
|
|
const radien = tiefe.ecken.split(/\s+/).map(parseFloat);
|
|
pruefe("die Ecken sind ungleich geschliffen",
|
|
new Set(radien).size > 1, tiefe.ecken);
|
|
pruefe("und ein Glanzstreifen liegt auf der Platte",
|
|
/gradient/.test(tiefe.glanz), tiefe.glanz);
|
|
|
|
console.log("\n===== 4. WECHSELT DER RAUM WIRKLICH? =====");
|
|
const gesehen = [];
|
|
for (const bereich of BEREICHE) {
|
|
await bereichAufbauen(seite, bereich);
|
|
const d = await seite.evaluate(() => {
|
|
const st = getComputedStyle(document.querySelector(".vw-buehne"));
|
|
const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
|
|
return {
|
|
datei: t ? t[1] : "(keins)",
|
|
farbe: getComputedStyle(document.documentElement)
|
|
.getPropertyValue("--vw-farbe").trim(),
|
|
};
|
|
});
|
|
gesehen.push({ bereich, ...d });
|
|
console.log(` ${bereich.padEnd(11)} ${d.datei.padEnd(22)} ${d.farbe}`);
|
|
}
|
|
pruefe("jeder Bereich hat eine eigene Leitfarbe",
|
|
new Set(gesehen.map((g) => g.farbe)).size === 6,
|
|
new Set(gesehen.map((g) => g.farbe)).size + " von 6");
|
|
pruefe("mindestens fuenf verschiedene Motive",
|
|
new Set(gesehen.map((g) => g.datei)).size >= 5,
|
|
new Set(gesehen.map((g) => g.datei)).size + " verschiedene");
|
|
|
|
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
|
|
await bereichAufbauen(seite, "kunden");
|
|
await seite.screenshot({ path: "pruef-vw-schmuck-computer.png" });
|
|
await seite.close();
|
|
|
|
console.log("\n===== 5. REDUZIERTE BEWEGUNG =====");
|
|
const ruhig = await browser.newPage({
|
|
viewport: { width: 1440, height: 900 }, reducedMotion: "reduce" });
|
|
await ruhig.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
|
|
await ruhig.waitForTimeout(1300);
|
|
await bereichAufbauen(ruhig, "projekte");
|
|
const bewegung = await ruhig.evaluate(() => {
|
|
const st = getComputedStyle(document.querySelector(".vw-buehne"));
|
|
return { animation: st.animationName, sichtbar: st.visibility };
|
|
});
|
|
pruefe("keine Animation bei reduzierter Bewegung",
|
|
bewegung.animation === "none", bewegung.animation);
|
|
/* Abschalten heisst NICHT verschwinden. */
|
|
pruefe("die Buehne ist trotzdem da", bewegung.sichtbar === "visible", bewegung.sichtbar);
|
|
await ruhig.mouse.move(200, 700, { steps: 10 });
|
|
await ruhig.waitForTimeout(400);
|
|
await ruhig.mouse.move(1200, 200, { steps: 16 });
|
|
await ruhig.waitForTimeout(600);
|
|
const ruhigeLage = await ruhig.evaluate(() => {
|
|
const b = document.querySelector(".vw-buehne");
|
|
const vor = getComputedStyle(b, "::before");
|
|
return {
|
|
translate: getComputedStyle(b).translate,
|
|
reflex: vor.animationName,
|
|
reflexSichtbar: parseFloat(vor.opacity),
|
|
};
|
|
});
|
|
pruefe("keine Parallaxe bei reduzierter Bewegung",
|
|
ruhigeLage.translate === "none", ruhigeLage.translate);
|
|
const ruhigExtra = await ruhig.evaluate(() => {
|
|
const be = document.querySelector("#vw-bereich");
|
|
const k = document.createElement("button");
|
|
k.className = "vw-karte";
|
|
be.appendChild(k);
|
|
const l = document.querySelector("#vw-bereich .vw-tabs");
|
|
return {
|
|
kachel: getComputedStyle(k).animationName,
|
|
balken: getComputedStyle(l, "::after").transitionDuration,
|
|
};
|
|
});
|
|
const ruhigNeigung = await ruhig.evaluate(() => {
|
|
const be = document.querySelector("#vw-bereich");
|
|
let k = be.querySelector(".vw-karte");
|
|
if (!k) { k = document.createElement("button"); k.className = "vw-karte"; be.appendChild(k); }
|
|
return getComputedStyle(k).transform;
|
|
});
|
|
pruefe("keine Neigung bei reduzierter Bewegung",
|
|
ruhigNeigung === "none", ruhigNeigung);
|
|
|
|
pruefe("keine Auftauch-Animation der Kacheln",
|
|
ruhigExtra.kachel === "none", ruhigExtra.kachel);
|
|
/* Geprueft wird "praktisch sofort", nicht wortwoertlich "0s".
|
|
|
|
Die Testumgebung emuliert reduzierte Bewegung, indem sie alle
|
|
Uebergaenge auf eine Mikrosekunde setzt statt auf null -- gemeldet
|
|
wird dann "1e-06s". Wahrnehmbar ist das identisch; ein Test auf
|
|
exakt "0s" wuerde nur die Emulation messen, nicht die Regel. */
|
|
const laengste = Math.max(...ruhigExtra.balken.split(",")
|
|
.map((t) => parseFloat(t) || 0));
|
|
pruefe("und der Balken gleitet nicht, sondern springt",
|
|
laengste <= 0.01, ruhigExtra.balken);
|
|
|
|
const ruhigFlocken = await ruhig.evaluate(() => {
|
|
const f = document.querySelector(".vw-flocken");
|
|
return f ? getComputedStyle(f).display : "(fehlt)";
|
|
});
|
|
/* Komplett weg, nicht nur angehalten: Ein eingefrorener Punkt mitten
|
|
im Bild waere ein Fleck ohne Sinn. */
|
|
pruefe("keine Partikel bei reduzierter Bewegung",
|
|
ruhigFlocken === "none", ruhigFlocken);
|
|
|
|
pruefe("und kein Lichtreflex",
|
|
ruhigeLage.reflex === "none" && ruhigeLage.reflexSichtbar === 0,
|
|
ruhigeLage.reflex + ", Deckkraft " + ruhigeLage.reflexSichtbar);
|
|
await ruhig.close();
|
|
|
|
console.log("\n===== 6. HANDY =====");
|
|
const handy = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
await handy.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
|
|
await handy.waitForTimeout(1300);
|
|
await bereichAufbauen(handy, "zahlungen");
|
|
const hd = await handy.evaluate(() => {
|
|
const st = getComputedStyle(document.querySelector(".vw-buehne"));
|
|
const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
|
|
return { datei: t ? t[1] : "(keins)", groesse: st.backgroundSize };
|
|
});
|
|
pruefe("Handy: kleine Bildfassung", hd.datei.includes("-klein"), hd.datei);
|
|
pruefe("Handy: auch hier kein Zoom", hd.groesse === "cover", hd.groesse);
|
|
const ueberlauf = await handy.evaluate(() =>
|
|
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
pruefe("Handy: nichts laeuft seitlich ueber", ueberlauf <= 1, ueberlauf + "px");
|
|
await handy.screenshot({ path: "pruef-vw-schmuck-handy.png" });
|
|
await handy.close();
|
|
|
|
const verpasst = fehlt.filter((f) => /\.(webp|jpg|png|css|js|svg)$/.test(f));
|
|
pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine");
|
|
|
|
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
|
|
await browser.close();
|
|
srv.close();
|
|
process.exit(fehl ? 1 : 0);
|
|
});
|