Files
dogfather-universe/pruef-rabattcodes.mjs
T
DogFatherGitandClaude Opus 5 5a67ef2948 Die Rabattcodes haengen am Konto, nicht mehr am Abo
Filipe: "die partner codes sollen auch schon fuer die leute sichtbar sein
die angemeldet sind." Umgestellt, und auf Nachfrage dauerhaft: Rabattcodes
sind ab jetzt ein Konto-Vorteil, kein Abo-Vorteil.

WARUM DAS MEHR IST ALS EINE BEQUEMLICHKEIT

Der alte Riegel verlangte einen Abo-Status. Nachgesehen, statt vermutet:
Die Bezahlung auf abonnieren.html steht auf "Coming soon", bis Dogis
PayPal-Business-Zugang da ist (der Kommentar dort nennt es beim Namen).
Registrieren geht, bezahlen nicht. Der erste echte Partnercode lag damit
seit gestern hinter einer Tuer, die sich gar nicht oeffnen laesst -- er
waere fuer NIEMANDEN sichtbar gewesen ausser fuer Dogi und VanVan ueber
die Rollenvorschau.

Entschieden wird jetzt an "supporterToken" (beim Login gesetzt, beim
Logout entfernt, supporter.js). Der Abo-Stand wird als Sicherheitsnetz
weiter mitgelesen: Niemand soll Zugang verlieren, den er gestern hatte.

BEIDE STELLEN, NICHT EINE

Die Bedingung steht doppelt im Haus -- an der Kachel auf links.html und
an der Seite selbst. Nur eine davon umzustellen erzeugt einen Fehler, den
keine der beiden fuer sich zeigt: Man kaeme mit Konto auf die Seite und
saehe dort die Sperre. Beide sind umgestellt, tragen den Hinweis
aufeinander, und die Pruefung vergleicht sie in jedem Anmeldezustand
gegeneinander.

TEXTE, DIE SONST GELOGEN HAETTEN

"Nur fuer Supporter" auf einer Seite, die ein kostenloses Konto oeffnet,
schickt Leute zum Bezahlen fuer etwas, das sie umsonst bekommen. Kopf,
Vorspann, Kachelband, Beschreibung und Sperrtext sagen jetzt "Konto", in
allen fuenf Sprachen. Die Sperre bietet auf Filipes Wunsch beide Wege an:
den kostenlosen zuerst, das Abo daneben -- mit einer Zeile darunter, dass
es erst startet, wenn es offiziell live geht. Ohne die waere der zweite
Knopf eine Falle.

EIN FEHLER, DER SEIT DEM 03.08.2026 DRINSTAND

Die Pruefung meldete auf der FREIGESCHALTETEN Kachel weiter "Nur mit
Konto" statt "Freigeschaltet". Ursache: Das Skript setzte den Text
(`badge.textContent = ...`), aber applyTranslations() schreibt aus dem
data-i18n-Attribut zurueck -- und es laeuft danach noch einmal, weil
dogiSiteTexteLaden() die Texte aus der Verwaltung holt und dann neu
uebersetzt.

NACHGEMESSEN STATT HERGELEITET, und die erste Erklaerung war zu schnell:
Der Text war schon nach 50 ms falsch, also nicht "irgendwann spaeter
ueberschrieben". Der Grund ist, dass TEAM_API_BASIS auf den ECHTEN Worker
zeigt -- der Abruf gelingt selbst aus einer lokalen Testseite. Kontroll-
versuch mit blockiertem Abruf: derselbe alte Code, und das Band bleibt
korrekt. Ursache weg, Fehler weg.

Behoben, indem der SCHLUESSEL getauscht wird statt des Textes. Damit
schreibt jeder weitere Uebersetzungslauf von selbst das Richtige hin --
auch bei Sprachwechsel, wo die alte Fassung ebenfalls zurueckfiel.
Aufgefallen ist es nie, weil bis gestern niemand in den freigeschalteten
Zustand kommen konnte.

NEBENBEFUND, NICHT ANGEFASST: index.html hat dieselbe Bauart beim
Live-Status (#live-text mit data-i18n, Text per Skript gesetzt).
Gemessen ist es dort ein Wettlauf zweier Abrufe -- in meinem Lauf gewann
der Status um Haaresbreite, und ein Sprachwechsel repariert es dort
ohnehin (dogi-sprache-geaendert). Kleiner, aber echt. Auf Ansage.

pruef-rabattcodes EXIT=0 (63 Pruefungen, vorher 42). Neu darunter: drei
Anmeldezustaende statt zweier -- ausgeloggt, angemeldet ohne Abo,
angemeldet mit Abo --, jeder auf BEIDEN Seiten, dazu der Klick auf die
Kachel (fuehrt sie wirklich weiter?), die Beschriftung des Bands und als
Gegenprobe ein erzwungener Uebersetzungslauf, der den alten Fehler
zuverlaessig ausloest.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 21:49:16 +02:00

468 lines
23 KiB
JavaScript

/* Prüft die Rabattcodes mit dem ersten echten Partnercode (DOGI10) —
auf BEIDEN Seiten, auf denen sie vorkommen.
WARUM ZWEI SEITEN IN EINER PRÜFUNG
Die Bedingung "wer darf die Codes sehen" steht doppelt im Haus: in
links.html an der Kachel und in rabattcodes.html an der Seite selbst.
Ändert jemand nur eine davon, entsteht ein Fehler, den keine der
beiden für sich zeigt — man kommt mit Konto zwar auf die Seite und
sieht dort die Sperre, oder die Kachel blockiert jemanden, der die
Codes sehen dürfte. Geprüft wird deshalb beides gegeneinander.
WAS HIER SCHIEFGEHEN KANN, UND WARUM MAN ES NICHT SIEHT
Man bekommt immer nur einen Zustand zu sehen. Wer im Browser
nachschaut, prüft also ein Drittel. Alle drei stehen hier drin:
ausgeloggt, angemeldet ohne Abo, angemeldet mit Abo. Der mittlere ist
der, um den es seit dem 08.09.2026 geht (Filipe: "die partner codes
sollen auch schon für die leute sichtbar sein die angemeldet sind") —
ohne ihn prüfte man weiter nur die zwei, die schon vorher gingen.
Dazu jeweils der KLICK: Eine Kachel, die richtig aussieht und
trotzdem nicht weiterführt, fällt sonst niemandem auf.
Dazu vier Dinge, die im Bild richtig aussehen und trotzdem falsch
sein können:
* Das Siegel. Ein nicht freigestelltes Logo ist auf einer dunklen
Karte kaum zu erkennen — der schwarze Kasten geht im Hintergrund
unter. Genau daran ist die Workspace-Marke schon einmal
vorbeigerutscht ("60,8 % deckend" klang tadellos). Hier werden
deshalb die Bildpunkte der AUSGELIEFERTEN Datei gemessen: Ecken
durchsichtig, Mitte deckend.
* Der Code. "DOGI10" ist kurz genug, um ihn beim Abtippen zu
verdrehen, und lang genug, dass es niemandem auffällt. Verglichen
wird deshalb zeichengenau, nicht "enthält".
* Das Ablaufdatum. Es steht an zwei Stellen — sichtbar im Text und
als Zahl im Skript. Laufen die auseinander, verspricht die Seite
etwas, das sie selbst schon anders bewertet. Die Prüfung liest
beide und vergleicht sie.
* Die Ablaufschaltung selbst. Sie wird erst am 01.10.2026 zum
ersten Mal gebraucht — also genau dann, wenn niemand mehr
hinschaut. Sie wird hier mit gestellter Uhr in beide Richtungen
gefahren: am letzten gültigen Abend muss der Code GELTEN, am
Morgen danach nicht mehr. Eine Prüfung, die nur bestätigen kann,
bestätigt nichts.
Läuft rein lokal gegen die Dateien im Arbeitsverzeichnis, rund 20
Sekunden. Kein Zugriff auf den Server, keine Bestellung, keine Daten.
Aufrufen mit: node pruef-rabattcodes.mjs
*/
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync, statSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
/* Auf isFile() statt auf "endet nicht mit /" prüfen: main.js fragt auch
Adressen ohne Schrägstrich am Ende ab, die im Arbeitsverzeichnis ein
ORDNER sind. readFileSync bricht darauf mit EISDIR ab -- und weil das
im Serverrückruf passiert, stirbt der ganze Prüflauf statt einer
einzelnen Anfrage. */
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && statSync(p).isFile()) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4331, r));
const BASIS = "http://127.0.0.1:4331";
/* Erwartungswerte. Sie stammen aus dem Shop, nicht aus dem Codenamen:
/home/vandiy/vans-diy-bastelbedarf/src/content/partnercodes.json
{ "code": "DOGI10", "prozent": 10, "bis": "2026-09-30",
"aktiv": true, "giltAufSale": false }
Ändert VanVan die Konditionen, muss hier UND auf der Seite nachgezogen
werden — dass beides zusammen auffällt, ist der Zweck dieser Zeilen. */
const CODE = "DOGI10";
const SHOP = "https://vans-diy-bastelbedarf.com/";
const BIS = "2026-09-30";
const BIS_TEXT = "30.09.2026";
const ergebnisse = [];
function pruefe(name, ok, zusatz) {
ergebnisse.push({ name, ok });
console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : ""));
}
const browser = await chromium.launch();
/* DIE DREI ANMELDEZUSTAENDE, die im Browser wirklich vorkommen.
supporter.js legt beim Login "supporterToken" an und stellt den
Abo-Stand als "supporterSubStatus" daneben -- "none", wenn ein Konto
besteht, aber nichts bezahlt ist (supporter.html, /supporter/me).
Beim Logout verschwinden beide zusammen (clearToken).
Der MITTLERE Zustand ist der, um den es seit dem 08.09.2026 geht: Wer
nur ein Konto hat, muss die Codes sehen. Ohne ihn pruefte man weiter
nur die zwei Zustaende, die auch vorher schon funktioniert haben. */
const ZUSTAENDE = [
{ schluessel: "aus", name: "ausgeloggt", token: null, status: null, darfSehen: false },
{ schluessel: "konto", name: "angemeldet, kein Abo", token: "pruef-tok", status: "none", darfSehen: true },
{ schluessel: "abo", name: "angemeldet mit Abo", token: "pruef-tok", status: "active", darfSehen: true },
];
const zustandVon = (k) => ZUSTAENDE.find((z) => z.schluessel === k);
/* Eine Seite im gewünschten Zustand öffnen. `zeit` stellt die Uhr (nur
Date, die Zeitgeber laufen weiter — sonst stünde main.js still). */
async function seiteOeffnen({ zustand = "konto", seite = "/rabattcodes.html",
zeit = null, sprache = "de", viewport, reducedMotion } = {}) {
const z = zustandVon(zustand);
const ctx = await browser.newContext({
viewport: viewport || { width: 1280, height: 1000 },
permissions: ["clipboard-read", "clipboard-write"],
reducedMotion,
});
const p = await ctx.newPage();
if (zeit) await p.clock.setFixedTime(zeit);
await p.addInitScript(([token, status, spr]) => {
if (token) localStorage.setItem("supporterToken", token);
else localStorage.removeItem("supporterToken");
if (status) localStorage.setItem("supporterSubStatus", status);
else localStorage.removeItem("supporterSubStatus");
localStorage.setItem("dogi-sprache", spr);
}, [z.token, z.status, sprache]);
await p.goto(BASIS + seite, { waitUntil: "domcontentloaded" });
await p.waitForTimeout(700);
return { ctx, p };
}
/* ===================================================================
1 — Wer die Codes sieht: drei Zustaende, ZWEI Seiten
Die Bedingung steht doppelt im Haus (links.html und rabattcodes.html)
und muss an beiden Stellen gleich lauten. Laeuft nur eine davon auf,
entsteht ein Fehler, den keine der beiden Seiten fuer sich zeigt: Man
kommt mit Konto zwar auf die Seite, sieht dort aber die Sperre -- oder
die Kachel blockiert jemanden, der die Codes sehen duerfte. Deshalb
wird hier nicht nur jede Seite geprueft, sondern auch, dass beide
dasselbe sagen.
=================================================================== */
console.log("\n=== Wer die Codes sieht (drei Anmeldezustände, beide Seiten) ===");
for (const z of ZUSTAENDE) {
const soll = z.darfSehen;
const a = await seiteOeffnen({ zustand: z.schluessel });
const r = await a.p.evaluate(() => ({
karte: !document.getElementById("rc-inhalt").hidden,
sperre: !document.getElementById("rc-gesperrt").hidden,
codeSichtbar: !!document.getElementById("rc-code")?.checkVisibility?.(),
}));
pruefe(`[${z.name}] rabattcodes.html zeigt ${soll ? "die Codes" : "die Sperre"}`,
r.karte === soll && r.sperre === !soll, `Karte=${r.karte} Sperre=${r.sperre}`);
/* Gegenprobe: "hidden" ist ein Attribut, kein Beweis. */
pruefe(`[${z.name}] rabattcodes.html: Code ist ${soll ? "sichtbar" : "unsichtbar"}`,
r.codeSichtbar === soll, `sichtbar=${r.codeSichtbar}`);
await a.ctx.close();
const b = await seiteOeffnen({ zustand: z.schluessel, seite: "/links.html" });
const l = await b.p.evaluate(() => ({
gesperrt: document.getElementById("card-rabattcodes").classList.contains("gesperrt"),
band: document.getElementById("rabatt-badge")?.textContent.trim(),
}));
pruefe(`[${z.name}] links.html: Kachel ist ${soll ? "offen" : "gesperrt"}`,
l.gesperrt === !soll, `gesperrt=${l.gesperrt}, Band „${l.band}“`);
/* Das BAND muss zur Kachel passen. Ein Schloss auf einer offenen Kachel
sagt dem Angemeldeten das Gegenteil von dem, was gilt -- und genau das
stand dort: Der Text wurde per Skript gesetzt, aber applyTranslations()
schrieb ihn aus dem data-i18n-Attribut wieder zurueck. */
pruefe(`[${z.name}] links.html: Band sagt „${soll ? "✓ Freigeschaltet" : "🔒 Nur mit Konto"}“`,
l.band === (soll ? "✓ Freigeschaltet" : "🔒 Nur mit Konto"), l.band);
/* GEGENPROBE GEGEN GENAU DEN RUECKFALL: eine erneute Uebersetzung.
Der Sprachwechsel loest applyTranslations() aus, ohne Netz und ohne
Warten -- mit der alten Fassung stand danach wieder das Schloss da.
Eine Pruefung, die den Fehler nicht ausloesen kann, haette ihn auch
nicht gefunden. */
const nachWechsel = await b.p.evaluate(() => {
document.querySelector(".sprach-schalter [data-sprache='en']").click();
return document.getElementById("rabatt-badge").textContent.trim();
});
pruefe(`[${z.name}] links.html: Band überlebt eine erneute Übersetzung`,
nachWechsel === (soll ? "✓ Unlocked" : "🔒 Account required"), nachWechsel);
pruefe(`[${z.name}] beide Seiten sind sich einig`,
l.gesperrt === !r.karte, `links gesperrt=${l.gesperrt}, rabattcodes offen=${r.karte}`);
/* Und was passiert beim KLICK? Das ist die Frage, um die es geht --
eine Kachel, die richtig aussieht und trotzdem nicht weiterfuehrt,
haetten die Zeilen oben nicht bemerkt. */
await b.p.click("#card-rabattcodes");
await b.p.waitForTimeout(700);
const nach = b.p.url();
if (soll) {
pruefe(`[${z.name}] Klick auf die Kachel führt zu den Codes`,
nach.endsWith("/rabattcodes.html"), nach);
} else {
const hinweisDa = await b.p.evaluate(() =>
!document.getElementById("rabatt-gesperrt-hinweis").hidden);
pruefe(`[${z.name}] Klick führt nicht weiter, sondern zeigt den Hinweis`,
nach.endsWith("/links.html") && hinweisDa, `${nach}, Hinweis=${hinweisDa}`);
}
await b.ctx.close();
}
/* ===================================================================
1b — Die Sperre bietet BEIDE Wege an (Wunsch 08.09.2026)
Der alte Knopf schickte Ausgeloggte allein aufs Abo. Das ist noch gar
nicht bezahlbar ("Coming soon" auf abonnieren.html), und fuer die
Codes reicht ein kostenloses Konto -- der Weg dorthin muss deshalb
der erste sein.
=================================================================== */
console.log("\n=== Zwei Wege für Ausgeloggte ===");
for (const [seite, wahl] of [["/rabattcodes.html", "#rc-gesperrt .btn"],
["/links.html", "#rabatt-gesperrt-hinweis .btn"]]) {
const { ctx, p } = await seiteOeffnen({ zustand: "aus", seite });
if (seite.endsWith("links.html")) await p.click("#card-rabattcodes");
await p.waitForTimeout(400);
const knoepfe = await p.$$eval(wahl, (els) =>
els.map((e) => ({ text: e.textContent.trim(), href: e.getAttribute("href") })));
const zumKonto = knoepfe.filter((k) => k.href === "abonnieren.html");
pruefe(`${seite}: Weg zum Konto UND zum Abo vorhanden`, zumKonto.length === 2,
knoepfe.map((k) => `${k.text} -> ${k.href}`).join(" | "));
pruefe(`${seite}: der erste Weg ist der kostenlose`,
/registrier|anmeld/i.test(zumKonto[0] ? zumKonto[0].text : ""), zumKonto[0] && zumKonto[0].text);
await ctx.close();
}
/* ===================================================================
2 — Das Siegel: geladen, gross genug, wirklich freigestellt
=================================================================== */
console.log("\n=== Partnersiegel ===");
{
const { ctx, p } = await seiteOeffnen();
await p.waitForFunction(() => {
const i = document.querySelector(".rc-medaillon img");
return i && i.complete && i.naturalWidth > 0;
}, null, { timeout: 8000 }).catch(() => {});
const b = await p.evaluate(() => {
const i = document.querySelector(".rc-medaillon img");
if (!i) return { fehler: "kein Bild" };
const r = i.getBoundingClientRect();
const K = 64;
const c = document.createElement("canvas");
c.width = K; c.height = K;
const s = c.getContext("2d", { willReadFrequently: true });
s.drawImage(i, 0, 0, K, K);
const d = s.getImageData(0, 0, K, K).data;
const alpha = (x, y) => d[(y * K + x) * 4 + 3];
let deckend = 0;
for (let k = 3; k < d.length; k += 4) if (d[k] > 200) deckend++;
return {
quelle: (i.currentSrc || i.src).split("/").pop(),
natur: i.naturalWidth, breite: Math.round(r.width),
ecken: [alpha(0, 0), alpha(K - 1, 0), alpha(0, K - 1), alpha(K - 1, K - 1)],
mitte: alpha(K >> 1, K >> 1),
anteil: deckend / (K * K),
};
});
pruefe("Siegel geladen (naturalWidth 384)", b.natur === 384, `${b.quelle} / ${b.natur}px`);
pruefe("Siegel wird gross genug angezeigt (>= 110 px)", b.breite >= 110, `${b.breite} px`);
/* DAS ist die Prüfung, die den schwarzen Kasten gefunden hätte. */
pruefe("Ecken des Siegels sind durchsichtig", b.ecken.every((a) => a <= 20), b.ecken.join("/"));
pruefe("Mitte des Siegels ist deckend", b.mitte >= 250, String(b.mitte));
/* Und die Anzahl gehört in die Bedingung, nicht in den Meldetext: ein
leeres Bild hätte alle drei Prüfungen oben bestanden. Ein Kreis im
Quadrat deckt pi/4, hier mit 6 % Rand rund 70 %. */
pruefe("Es ist auch wirklich ein rundes Siegel drauf (60-80 % Fläche)",
b.anteil > 0.6 && b.anteil < 0.8, `${(b.anteil * 100).toFixed(1)} %`);
await ctx.close();
}
/* ===================================================================
3 — Code, Shoplink, Konditionen
=================================================================== */
console.log("\n=== Code und Konditionen ===");
{
const { ctx, p } = await seiteOeffnen();
const z = await p.evaluate(() => {
const knopf = document.getElementById("rc-code");
const link = document.querySelector('.rc-partner a.btn-primary');
return {
wert: document.querySelector(".rc-code-wert")?.textContent.trim(),
daten: knopf?.getAttribute("data-code"),
href: link?.getAttribute("href"),
ziel: link?.getAttribute("target"),
rel: link?.getAttribute("rel"),
fakten: [...document.querySelectorAll(".rc-fakten li")].map((l) => l.textContent.trim()),
skript: [...document.querySelectorAll("script:not([src])")].map((s) => s.textContent).join("\n"),
};
});
pruefe(`Angezeigter Code lautet genau „${CODE}“`, z.wert === CODE, z.wert);
/* Kopiert wird data-code, angezeigt wird der Text. Beide müssen
dasselbe sagen — sonst kopiert man etwas anderes, als man liest. */
pruefe("Angezeigter Code und kopierter Code sind identisch", z.daten === z.wert, `${z.daten} / ${z.wert}`);
pruefe(`Shoplink zeigt genau auf ${SHOP}`, z.href === SHOP, z.href);
pruefe("Shoplink öffnet in neuem Tab und ohne Rückreferenz",
z.ziel === "_blank" && (z.rel || "").includes("noopener"), `${z.ziel} / ${z.rel}`);
pruefe("Drei Konditionen stehen da", z.fakten.length === 3, z.fakten.length + "");
pruefe("Rabatthöhe genannt (10 %)", z.fakten.some((f) => /10\s?%/.test(f)), z.fakten[0]);
pruefe(`Ablaufdatum genannt (${BIS_TEXT})`, z.fakten.some((f) => f.includes(BIS_TEXT)), z.fakten[1]);
pruefe("Sale-Ausnahme genannt", z.fakten.some((f) => /reduziert/i.test(f)), z.fakten[2]);
/* Der Text sagt ein Datum, das Skript rechnet mit einem. Zwei Stellen,
eine Wahrheit — hier wird sie verglichen. */
const imSkript = (z.skript.match(/var ABLAUF = "([\d-]+)"/) || [])[1];
pruefe(`Skript rechnet mit demselben Datum wie der Text`,
imSkript === BIS, `Skript ${imSkript}, Text ${BIS_TEXT}`);
await ctx.close();
}
/* ===================================================================
4 — Kopieren, mit Gegenprobe
=================================================================== */
console.log("\n=== Kopieren ===");
{
const { ctx, p } = await seiteOeffnen();
/* Vorher: die Erfolgsmeldung darf NICHT stehen. Ohne diese Zeile
würde eine dauerhaft sichtbare Meldung als Erfolg durchgehen. */
const vorher = await p.evaluate(() => !document.getElementById("rc-echo-ok").hidden);
pruefe("Vor dem Klick steht keine Erfolgsmeldung", vorher === false, String(vorher));
await p.click("#rc-code");
await p.waitForTimeout(400);
const nachher = await p.evaluate(async () => ({
meldung: !document.getElementById("rc-echo-ok").hidden,
fehler: !document.getElementById("rc-echo-fehler").hidden,
marke: document.getElementById("rc-code").classList.contains("ist-kopiert"),
zwischenablage: await navigator.clipboard.readText(),
}));
pruefe(`In der Zwischenablage steht „${CODE}“`, nachher.zwischenablage === CODE, nachher.zwischenablage);
pruefe("Erfolgsmeldung erscheint", nachher.meldung === true && nachher.fehler === false,
`ok=${nachher.meldung} fehler=${nachher.fehler}`);
pruefe("Knopf zeigt den kopierten Zustand", nachher.marke === true, String(nachher.marke));
await ctx.close();
}
/* ===================================================================
5 — Ablauf, in beide Richtungen (gestellte Uhr)
=================================================================== */
console.log("\n=== Ablaufschaltung ===");
for (const [wann, zeit, sollAbgelaufen] of [
["am letzten gültigen Abend", new Date(`${BIS}T21:30:00`), false],
["am Morgen danach", new Date("2026-10-01T09:00:00"), true],
]) {
const { ctx, p } = await seiteOeffnen({ zeit });
const z = await p.evaluate(() => ({
markiert: document.getElementById("rc-partner").classList.contains("ist-abgelaufen"),
hinweis: !document.getElementById("rc-abgelaufen").hidden,
gesperrt: document.getElementById("rc-code").disabled,
}));
pruefe(`${wann}: Code gilt ${sollAbgelaufen ? "nicht mehr" : "noch"}`,
z.markiert === sollAbgelaufen && z.hinweis === sollAbgelaufen && z.gesperrt === sollAbgelaufen,
`markiert=${z.markiert} hinweis=${z.hinweis} gesperrt=${z.gesperrt}`);
await ctx.close();
}
/* ===================================================================
6 — Alle fünf Sprachen
=================================================================== */
console.log("\n=== Sprachen ===");
{
const erwartet = {
de: ["Dein Code", "Tippen zum Kopieren"],
"de-CH": ["Din Code", "Tippe zum Kopiere"],
en: ["Your code", "Tap to copy"],
fr: ["Ton code", "Touche pour copier"],
pt: ["O teu código", "Toca para copiar"],
};
for (const [sp, [kopf, fuss]] of Object.entries(erwartet)) {
const { ctx, p } = await seiteOeffnen({ sprache: sp });
const z = await p.evaluate(() => ({
kopf: document.querySelector(".rc-code-kopf")?.textContent.trim(),
fuss: document.querySelector(".rc-code-fuss")?.textContent.trim(),
/* Ein nicht übersetzter Schlüssel erscheint wörtlich als "rc_..." —
das ist der sichtbare Rest, nach dem gesucht wird. */
reste: document.getElementById("rc-inhalt").innerText.match(/rc_[a-z_]+/g) || [],
entitaeten: document.querySelector(".rc-medaillon img")?.getAttribute("alt") || "",
}));
pruefe(`[${sp}] Beschriftungen übersetzt`, z.kopf === kopf && z.fuss === fuss, `${z.kopf} / ${z.fuss}`);
pruefe(`[${sp}] keine unübersetzten Schlüssel sichtbar`, z.reste.length === 0, z.reste.join(","));
/* Die Falle aus pruef-texte.mjs, hier für ein Attribut: Wörterbuch
-> setAttribute kodiert NICHT, "&amp;" stünde wörtlich im Alt-Text
und würde so vorgelesen. */
pruefe(`[${sp}] Alt-Text ohne Kodierungsrest`, !/&(amp|lt|gt|quot|#39);/.test(z.entitaeten), z.entitaeten);
await ctx.close();
}
}
/* ===================================================================
7 — Handy, Bewegungsarmut, Bildschirmfotos
=================================================================== */
console.log("\n=== Handy und Bewegungsarmut ===");
{
const { ctx, p } = await seiteOeffnen({ viewport: { width: 390, height: 844 } });
const z = await p.evaluate(() => {
const k = document.getElementById("rc-partner").getBoundingClientRect();
return {
quer: document.documentElement.scrollWidth - document.documentElement.clientWidth,
kartenbreite: Math.round(k.width),
knopfhoehe: Math.round(document.getElementById("rc-code").getBoundingClientRect().height),
};
});
pruefe("Handy 390 px: nichts läuft seitlich über", z.quer <= 0, `${z.quer} px Überhang`);
pruefe("Handy 390 px: Kopierknopf ist gross genug (>= 44 px)", z.knopfhoehe >= 44, `${z.knopfhoehe} px`);
await p.screenshot({ path: "pruef-rabattcodes-handy.png", fullPage: true });
await ctx.close();
}
{
const { ctx, p } = await seiteOeffnen({ reducedMotion: "reduce" });
const laufen = await p.evaluate(() => {
const teile = [".rc-medaillon", ".rc-glanz", ".rc-schliff", ".rc-code-wert"];
return teile.filter((s) => {
const el = document.querySelector(s);
return el && getComputedStyle(el).animationName !== "none";
});
});
pruefe("Bei „weniger Bewegung“ steht alles still", laufen.length === 0, laufen.join(","));
await ctx.close();
}
{
const { ctx, p } = await seiteOeffnen();
await p.waitForTimeout(500);
await p.locator("#rc-partner").screenshot({ path: "pruef-rabattcodes-karte.png" });
await ctx.close();
}
{
const { ctx, p } = await seiteOeffnen({ zustand: "aus" });
await p.screenshot({ path: "pruef-rabattcodes-gesperrt.png", fullPage: true });
await ctx.close();
}
await browser.close();
srv.close();
/* ===================================================================
Ergebnis — mit der ANZAHL in der Bedingung.
Ein grüner Lauf mit weniger Prüfungen als vorher ist kein grüner Lauf,
sondern ein stiller Aussetzer (Erkenntnis 28.08.2026). Die Zahl unten
wird beim Erweitern mit angehoben.
=================================================================== */
const MINDESTENS = 60;
const fehl = ergebnisse.filter((e) => !e.ok);
console.log(`\n${ergebnisse.length} Prüfungen, ${fehl.length} fehlgeschlagen.`);
console.log("Bildschirmfotos: pruef-rabattcodes-karte.png, -handy.png, -gesperrt.png");
if (ergebnisse.length < MINDESTENS) {
console.error(`FEHLER: nur ${ergebnisse.length} von mindestens ${MINDESTENS} Prüfungen gelaufen — `
+ `es ist etwas stillschweigend ausgefallen.`);
process.exit(1);
}
if (fehl.length) {
console.error("\nFehlgeschlagen:\n" + fehl.map((e) => " - " + e.name).join("\n"));
process.exit(1);
}
console.log("Alles in Ordnung.");