Die Kacheln nahmen bisher die volle Breite ein -- ueber 1180 Punkte pro Stueck. Jetzt stehen zwei nebeneinander, jede etwa 574 Punkte breit. ZWEI UMWEGE, DIE NICHT FUNKTIONIERT HABEN Zuerst stand hier auto-fit mit einer Mindestbreite von 420 Punkten. Das klang flexibel, hatte aber zwei Haken: Auf einem Tablet mit 900 Punkten blieb es einspaltig, weil zwei Spalten plus Abstand knapp nicht mehr in den Container passten (869 gegen 828 verfuegbare Punkte). Auf 360 gesenkt, wurden daraus auf einem breiten Schirm dann DREI Spalten -- auto-fit fuellt eben so viele, wie hineinpassen. Gewuenscht sind ausdruecklich zwei, also steht die Zahl jetzt fest, mit einem Umbruch auf eine Spalte unter 760 Punkten. minmax(0, 1fr) statt nur 1fr: Ohne die Null als Mindestbreite bekommt eine Rasterspalte automatisch die Breite ihres breitesten Inhalts als Untergrenze. Ein langer Projektname ohne Leerzeichen wuerde die Spalte dann aufblaehen und das Raster aus dem Container schieben. WAS SICH NEBENBEI VON SELBST ERLEDIGT Der Kippwinkel haengt an der Kachelgroesse. Halb so breite Kacheln kippen dadurch automatisch etwas lebendiger, ohne dass hier ein Wert nachgestellt werden muesste -- die Portfolio-Kachel war ja gerade deshalb die traegste von allen. Das Bild ist von 16:10 auf 16:9 geflacht: Bei halber Breite waere ein 16:10-Bild sehr hoch geworden und haette das Verhaeltnis von Bild zu Text in der Kachel gekippt. Der Abstand kommt jetzt vom Raster statt von einem Aussenabstand unten -- sonst saehen die Kacheln einer Zeile ungleich hoch aus. GEPRUEFT UEBER SECHS BILDSCHIRMBREITEN 1920px 2 Spalten Kachel 574px 1440px 2 Spalten Kachel 574px 1200px 2 Spalten Kachel 538px 900px 2 Spalten Kachel 403px 700px 1 Spalte Kachel 644px 390px 1 Spalte Kachel 359px Jeweils mit Gegenprobe, dass nichts seitlich ueberlaeuft. Ein Test auf nur einer Breite haette den Dreispalten-Fall nie bemerkt. Geprueft: 310 Pruefungen gruen (Portfolio 31, Bewegung 34, Verwaltung 62, CRYONOVA 53, Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54). Co-Authored-By: Claude Opus 5 <[email protected]>
294 lines
13 KiB
JavaScript
294 lines
13 KiB
JavaScript
/* =====================================================================
|
|
Prueft die Portfolio-Seite nach den drei neuen Eintraegen.
|
|
|
|
Der wichtigste Test hier ist NICHT "sieht huebsch aus", sondern:
|
|
Hat jeder data-i18n-Schluessel im HTML auch eine Uebersetzung? Ein
|
|
fehlender Schluessel faellt im Deutschen nicht auf -- der Text steht
|
|
ja als Rueckfalltext im HTML -- und bricht erst auf, wenn jemand die
|
|
Sprache wechselt. Genau deshalb wird hier in ALLEN fuenf Sprachen
|
|
geprueft, nicht nur in einer.
|
|
===================================================================== */
|
|
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 : "")); }
|
|
};
|
|
|
|
/* Kontrast nach WCAG 2.2. Halbtransparente Flaechen muessen ueber
|
|
ihrem Untergrund verrechnet werden, sonst misst man Unsinn. */
|
|
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);
|
|
}
|
|
|
|
srv.listen(4403, async () => {
|
|
const browser = await chromium.launch();
|
|
const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } });
|
|
const skriptfehler = [];
|
|
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120)));
|
|
|
|
await seite.goto("http://127.0.0.1:4403/webdesign/portfolio.html");
|
|
await seite.waitForTimeout(1500);
|
|
|
|
/* Die Vorschaubilder tragen loading="lazy" -- der Browser laedt sie
|
|
erst, wenn sie in Sichtweite kommen. Ohne dieses Vorbeischauen misst
|
|
der Test "Bild nicht geladen" und meint damit nur, dass er selbst
|
|
nie hingesehen hat. */
|
|
await seite.evaluate(async () => {
|
|
for (const el of document.querySelectorAll(".pfo-bild")) {
|
|
el.scrollIntoView();
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
}
|
|
window.scrollTo(0, 0);
|
|
});
|
|
await seite.waitForTimeout(1200);
|
|
|
|
console.log("1. SIND ALLE FUENF PROJEKTE DA?");
|
|
const projekte = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".pfo-projekt")].map((a) => ({
|
|
titel: a.querySelector("h2")?.textContent.trim(),
|
|
bild: a.querySelector("img")?.getAttribute("src")?.replace(/.*\//, ""),
|
|
geladen: a.querySelector("img")?.naturalWidth > 0,
|
|
link: a.querySelector("a[href^='http']")?.href || null,
|
|
})));
|
|
pruefe("fuenf Projekte auf der Seite", projekte.length === 5, String(projekte.length));
|
|
for (const p of projekte) {
|
|
pruefe(` ${p.titel}: Vorschaubild geladen`, p.geladen, p.bild);
|
|
}
|
|
|
|
/* Die Buchhaltung bekommt bewusst KEINEN Live-Knopf: Dahinter liegt
|
|
nur eine Anmeldemaske, ein Knopf dorthin waere ein leeres
|
|
Versprechen. Das wird hier festgehalten, damit es niemand spaeter
|
|
"der Vollstaendigkeit halber" wieder einbaut. */
|
|
const buch = projekte.find((p) => /Buchhaltung/i.test(p.titel || ""));
|
|
pruefe("die Buchhaltung hat absichtlich keinen Live-Knopf", buch && !buch.link,
|
|
buch ? String(buch.link) : "nicht gefunden");
|
|
|
|
console.log("\n1b. STEHEN GENAU ZWEI KACHELN NEBENEINANDER?");
|
|
for (const [name, breite] of [["Gross", 1920], ["Computer", 1440],
|
|
["Laptop", 1200], ["Tablet", 900],
|
|
["klein", 700], ["Handy", 390]]) {
|
|
await seite.setViewportSize({ width: breite, height: 900 });
|
|
await seite.waitForTimeout(500);
|
|
const r = await seite.evaluate(() => {
|
|
const g = document.querySelector(".pfo-raster");
|
|
if (!g) return null;
|
|
const k = g.querySelector(".pfo-projekt");
|
|
return {
|
|
spalten: getComputedStyle(g).gridTemplateColumns.split(" ").length,
|
|
kachel: Math.round(k.getBoundingClientRect().width),
|
|
ueberlauf: document.documentElement.scrollWidth -
|
|
document.documentElement.clientWidth,
|
|
};
|
|
});
|
|
if (!r) { pruefe("Raster vorhanden", false, "kein .pfo-raster"); break; }
|
|
|
|
/* Zwei Spalten ab 900 Punkten, darunter untereinander. Unter 760
|
|
waeren zwei Spalten so schmal, dass Ueberschrift und Schild im
|
|
Kachelkopf umbrechen. */
|
|
const erwartet = breite >= 900 ? 2 : 1;
|
|
pruefe(` ${name} (${breite}px): ${erwartet} Spalte(n)`,
|
|
r.spalten === erwartet, r.spalten + " Spalten, Kachel " + r.kachel + "px");
|
|
pruefe(` ${name}: nichts laeuft seitlich ueber`,
|
|
r.ueberlauf <= 1, r.ueberlauf + "px");
|
|
}
|
|
await seite.setViewportSize({ width: 1440, height: 950 });
|
|
await seite.waitForTimeout(500);
|
|
|
|
console.log("\n2. UEBERSETZUNGEN — IN ALLEN FUENF SPRACHEN");
|
|
/* Geprueft werden die SEITENEIGENEN Schluessel (pf_, p1_-p5_,
|
|
seiten_titel). Navigations- und Markentexte liegen bewusst nicht
|
|
hier, sondern in wd-core.js -- sie gelten fuer alle Seiten und
|
|
werden von server/pruefe-webdesign-i18n.mjs abgedeckt. Sie hier
|
|
mitzuzaehlen hat nur Fehlalarm erzeugt.
|
|
|
|
Und geprueft wird das ERGEBNIS, nicht das Woerterbuch: Die Seite
|
|
wird wirklich auf jede der fuenf Sprachen umgeschaltet, danach darf
|
|
kein seiteneigener Text leer sein. */
|
|
const luecken = [];
|
|
for (const sprache of ["de", "de-CH", "en", "fr", "pt"]) {
|
|
const kaputt = await seite.evaluate((spr) => {
|
|
window.WD.setSprache(spr);
|
|
const raus = [];
|
|
const eigen = /^(pf_|p[1-5]_|seiten_titel)/;
|
|
for (const el of document.querySelectorAll("[data-i18n]")) {
|
|
const k = el.getAttribute("data-i18n");
|
|
if (eigen.test(k) && !el.textContent.trim()) raus.push(k + " (leer)");
|
|
}
|
|
for (const el of document.querySelectorAll("[data-i18n-attr]")) {
|
|
const k = el.getAttribute("data-i18n-attr").split(":")[1];
|
|
const wert = el.getAttribute("alt");
|
|
if (eigen.test(k) && !(wert && wert.trim())) raus.push(k + " (alt leer)");
|
|
}
|
|
return raus;
|
|
}, sprache);
|
|
kaputt.forEach((k) => luecken.push(sprache + ": " + k));
|
|
}
|
|
pruefe("in jeder Sprache ist jeder seiteneigene Text gefuellt",
|
|
luecken.length === 0, luecken.slice(0, 4).join(", ") || "alle fuenf Sprachen sauber");
|
|
|
|
/* Gegenprobe: Der Test darf nicht deshalb gruen sein, weil die
|
|
Umschaltung in Wahrheit gar nichts tut. */
|
|
const wechsel = await seite.evaluate(() => {
|
|
const el = document.querySelector('[data-i18n="pf_h1"]');
|
|
window.WD.setSprache("de");
|
|
const deutsch = el.textContent.trim();
|
|
window.WD.setSprache("en");
|
|
const englisch = el.textContent.trim();
|
|
window.WD.setSprache("de");
|
|
return { deutsch, englisch };
|
|
});
|
|
pruefe("die Umschaltung wirkt wirklich",
|
|
wechsel.deutsch !== wechsel.englisch, wechsel.englisch);
|
|
await seite.waitForTimeout(300);
|
|
|
|
console.log("\n3. DIE SEITE SPRICHT NICHT MEHR VON ZWEI PROJEKTEN");
|
|
const zahlwort = await seite.evaluate(() => {
|
|
const t = window.I18N_WD;
|
|
const treffer = [];
|
|
for (const [k, v] of Object.entries(t)) {
|
|
for (const [s, txt] of Object.entries(v)) {
|
|
if (/\b(Zwei|Two|Deux|Dois|Beide|Both|Ambos)\b/i.test(txt)) treffer.push(k + "/" + s);
|
|
}
|
|
}
|
|
return treffer;
|
|
});
|
|
pruefe("keine Stelle behauptet noch 'zwei' oder 'beide'", zahlwort.length === 0,
|
|
zahlwort.join(", ") || "sauber");
|
|
|
|
console.log("\n4. LESBARKEIT DER NEUEN SCHILDER");
|
|
const schilder = await seite.evaluate(() => {
|
|
const holen = (sel) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return null;
|
|
const s = getComputedStyle(el);
|
|
/* Untergrund suchen: das erste Elternteil mit deckender Farbe. */
|
|
let p = el.parentElement, unter = null;
|
|
while (p && !unter) {
|
|
const bg = getComputedStyle(p).backgroundColor;
|
|
const m = bg.match(/[\d.]+/g);
|
|
if (m && (m.length === 3 || parseFloat(m[3]) === 1)) unter = m.slice(0, 3).map(Number);
|
|
p = p.parentElement;
|
|
}
|
|
return {
|
|
text: el.textContent.trim(),
|
|
farbe: s.color.match(/[\d.]+/g).slice(0, 3).map(Number),
|
|
eigen: s.backgroundColor.match(/[\d.]+/g),
|
|
unter: unter || [3, 8, 16],
|
|
};
|
|
};
|
|
return { aqua: holen(".wd-tag--aqua"), indigo: holen(".wd-tag--indigo") };
|
|
});
|
|
|
|
for (const [name, d] of Object.entries(schilder)) {
|
|
if (!d) { pruefe(`Schild ${name} gefunden`, false); continue; }
|
|
/* Eigene halbtransparente Flaeche ueber den Untergrund rechnen. */
|
|
const a = d.eigen.length === 4 ? parseFloat(d.eigen[3]) : 1;
|
|
const flaeche = d.unter.map((u, i) => Math.round(Number(d.eigen[i]) * a + u * (1 - a)));
|
|
const v = verhaeltnis(d.farbe, flaeche);
|
|
pruefe(`Schild "${d.text}" ist gut lesbar (AA = 4,5:1)`, v >= 4.5, v.toFixed(2) + ":1");
|
|
}
|
|
|
|
console.log("\n5. BEWEGEN SICH DIE BILDER?");
|
|
const bild = seite.locator(".pfo-bild").first();
|
|
await bild.scrollIntoViewIfNeeded();
|
|
await seite.waitForTimeout(800);
|
|
const rahmen = await bild.boundingBox();
|
|
|
|
/* Mit steps bewegen, sonst entsteht kein pointermove. */
|
|
await seite.mouse.move(rahmen.x + 30, rahmen.y + 30, { steps: 10 });
|
|
await seite.waitForTimeout(600);
|
|
const linksOben = await bild.evaluate((e) => ({
|
|
bx: parseFloat(e.style.getPropertyValue("--wd-bx")),
|
|
versatz: getComputedStyle(e).transform,
|
|
}));
|
|
await seite.mouse.move(rahmen.x + rahmen.width - 30,
|
|
rahmen.y + rahmen.height - 30, { steps: 18 });
|
|
await seite.waitForTimeout(650);
|
|
const rechtsUnten = await bild.evaluate((e) => ({
|
|
bx: parseFloat(e.style.getPropertyValue("--wd-bx")),
|
|
versatz: getComputedStyle(e).transform,
|
|
}));
|
|
|
|
/* Die Portfolio-Bilder stecken in einer Kachel. Dort bewegt sich die
|
|
KACHEL, das Bild bekommt nur den Zoom -- beides zusammen schaukelt
|
|
sich auf: Die kippende Kachel schiebt das Bild unter dem Zeiger
|
|
weg, der landet auf einem Nachbarelement, das Bild springt zurueck
|
|
auf null. Genau das war hier messbar: an einer Ecke sauber, an der
|
|
anderen dauerhaft 0.
|
|
|
|
Geprueft wird deshalb die Arbeitsteilung, nicht mehr die
|
|
Bildverschiebung. */
|
|
const holeX = (m) => parseFloat(m.slice(m.indexOf("(") + 1, -1).split(",")[4]);
|
|
const xLinks = holeX(linksOben.versatz);
|
|
const xRechts = holeX(rechtsUnten.versatz);
|
|
pruefe("das Bild in der Kachel verschiebt sich NICHT selbst",
|
|
Math.abs(xLinks) < 1 && Math.abs(xRechts) < 1,
|
|
xLinks.toFixed(1) + "px / " + xRechts.toFixed(1) + "px");
|
|
|
|
const kachel = await seite.locator(".pfo-projekt").first()
|
|
.evaluate((e) => getComputedStyle(e).transform);
|
|
pruefe("dafuer kippt die Kachel um das Bild herum",
|
|
kachel.startsWith("matrix3d"), kachel.slice(0, 24) + "...");
|
|
|
|
const zoom = parseFloat(rechtsUnten.versatz.slice(
|
|
rechtsUnten.versatz.indexOf("(") + 1, -1).split(",")[0]);
|
|
pruefe("und das Bild tritt leicht naeher", zoom > 1.01 && zoom < 1.12,
|
|
zoom.toFixed(3) + "-fach");
|
|
|
|
await seite.mouse.move(10, 10, { steps: 12 });
|
|
await seite.waitForTimeout(700);
|
|
const zurueck = await bild.evaluate((e) => e.style.getPropertyValue("--wd-bx"));
|
|
pruefe("beim Verlassen stellt es sich zurueck", zurueck === "0", zurueck);
|
|
|
|
console.log("\n6. KEINE FEHLER, KEINE FEHLENDEN DATEIEN");
|
|
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
|
|
const verpasst = fehlt.filter((f) => /\.(jpg|webp|png|css|js|svg)$/.test(f));
|
|
pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine");
|
|
|
|
await seite.screenshot({ path: "pruef-portfolio-computer.png", fullPage: true });
|
|
const handy = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
await handy.goto("http://127.0.0.1:4403/webdesign/portfolio.html");
|
|
await handy.waitForTimeout(1500);
|
|
const ueberlauf = await handy.evaluate(() =>
|
|
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
pruefe("auf dem Handy laeuft nichts seitlich ueber", ueberlauf <= 1, ueberlauf + "px");
|
|
await handy.screenshot({ path: "pruef-portfolio-handy.png", fullPage: true });
|
|
|
|
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
|
|
await browser.close();
|
|
srv.close();
|
|
process.exit(fehl ? 1 : 0);
|
|
});
|