Portfolio: ZockerAnstalt, Buchhaltung und Command Center aufgenommen
Aus zwei Projekten werden fuenf. Alle drei neuen laufen wirklich, die Adressen wurden vorher einzeln geprueft: zockeranstalt.dogfather-universe.com Netcup, via Caddy buchhaltung.vans-diy-bastelbedarf.com Netcup, via Caddy analyse.dogfather-universe.com Cloudflare Worker (kein via) Die Vorschaubilder sind echte Aufnahmen vom 24.08.2026, aufgenommen im selben Mass wie die bestehenden (1200x750), damit in der Liste nichts aus der Reihe faellt. Zwei Aufnahmen sind bewusst nicht unbearbeitet, und das steht auch fuer die Besucher auf der Seite -- nicht nur im Code: - Die Buchhaltung zeigt nur die Anmeldung. Sie bekommt deshalb auch KEINEN "Live ansehen"-Knopf: Ein Knopf, der bloss auf eine Anmeldemaske fuehrt, ist ein leeres Versprechen. Ein Test haelt das fest, damit es niemand spaeter "der Vollstaendigkeit halber" einbaut. - Beim Command Center sind die Gesichter unkenntlich gemacht. Die Seite gehoert dem Team, nicht mir, und niemand hat zugestimmt, sein Bild im Portfolio zu zeigen. Der Weichzeichner wurde vor der Aufnahme im Browser gesetzt, nicht nachtraeglich ins Bild gemalt. Zwei neue Schildfarben. Aqua traegt Schrift problemlos (11,5:1 gemessen). Indigo NICHT: Prism Indigo ist die einzige Farbe der Palette, die als Text durchfaellt (3,25--4,29:1) -- das Schild nutzt es deshalb nur als Flaeche und Kante, die Schrift bleibt Chrome Silver (9,8:1). Texte in allen fuenf Sprachen, inklusive Schweizerdeutsch. Ueberschrift, Einleitung und Schlusssatz sagen nicht mehr "zwei" bzw. "beide" -- und zwar sowohl im Woerterbuch ALS AUCH im deutschen Rueckfalltext im HTML. Nur die JS-Datei zu aendern haette gereicht, damit es in vier Sprachen stimmt und ausgerechnet auf Deutsch falsch bleibt. Drei Fehler steckten wieder in der Pruefung, nicht auf der Seite: - Sie meldete drei Vorschaubilder als "nicht geladen". Die tragen loading="lazy" -- der Test hatte schlicht nie hingesehen. - Sie zaehlte Navigations- und Markentexte mit. Die liegen aber in wd-core.js, gelten fuer alle Seiten und werden von server/pruefe-webdesign-i18n.mjs abgedeckt. - Ihre "kein Zwei mehr"-Suche haette auch Projekttexte getroffen, in denen das Wort voellig zurecht steht. Geprueft wird jetzt das Ergebnis statt des Woerterbuchs: Die Seite wird wirklich auf jede der fuenf Sprachen umgeschaltet, plus eine Gegenprobe, dass die Umschaltung ueberhaupt etwas tut. Geprueft: 198 Pruefungen gruen (Portfolio 15, CRYONOVA 53, Blickfang 13, System 5, Abmelden 16, Angebot 54, Abbruch 42). Die vier Meldungen aus server/pruefe-webdesign-i18n.mjs betreffen verwaltung.html und zwei fehlende Woerterbuecher -- Altbestand, keine dieser Dateien wurde hier angefasst. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/* =====================================================================
|
||||
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("\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. 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);
|
||||
});
|
||||
Reference in New Issue
Block a user