diff --git a/assets/img/marke/quelle-vandiy.jpg b/assets/img/marke/quelle-vandiy.jpg new file mode 100644 index 00000000..92db821a Binary files /dev/null and b/assets/img/marke/quelle-vandiy.jpg differ diff --git a/assets/img/partner-vandiy.webp b/assets/img/partner-vandiy.webp new file mode 100644 index 00000000..cd1e2d55 Binary files /dev/null and b/assets/img/partner-vandiy.webp differ diff --git a/assets/js/i18n-rabattcodes.js b/assets/js/i18n-rabattcodes.js index bf304c6d..5110bb7b 100644 --- a/assets/js/i18n-rabattcodes.js +++ b/assets/js/i18n-rabattcodes.js @@ -1,6 +1,20 @@ /* ===================================================================== i18n-rabattcodes.js — Übersetzungs-Wörterbuch NUR für rabattcodes.html Alle 5 Sprachen vollständig gepflegt (de, de-CH, en, fr, pt). + + ⚠️ ZWEI EINSETZWEGE, ZWEI SCHREIBWEISEN FÜR "&" (siehe main.js, + applyTranslations): + + data-i18n="schluessel" -> el.innerHTML = text + Dort gehört "&" hin. + data-i18n-attr="alt:schluessel" -> el.setAttribute(...) + Dort gehört ein blankes "&" hin; + "&" stünde wörtlich im + Vorlesetext. + + Betroffen ist hier genau ein Eintrag: rc_logo_alt (Attribut, blankes + "&"). Der Markenname selbst steht bewusst NICHT im Wörterbuch — "Van's + DIY & Bastelbedarf" heißt in jeder Sprache gleich. ===================================================================== */ window.I18N_PAGE = { @@ -23,39 +37,102 @@ window.I18N_PAGE = { de: "Nur für Dogfather-Supporter", "de-CH": "Nur für Dogfather-Supporter", en: "Dogfather supporters only", fr: "Réservé aux Supporters Dogfather", pt: "Somente para apoiadores do Dogfather", }, + /* Stand 08.09.2026 angepasst: Der Satz endete vorher auf "sobald die + ersten Kooperationen live sind". Seit heute IST die erste live — + der alte Text hätte einen Supporter dafür bezahlen lassen, auf etwas + zu warten, das schon hinter der Sperre liegt. */ rc_gesperrt_text: { - de: "Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Werde jetzt Teil davon, um sie freizuschalten, sobald die ersten Kooperationen live sind.", - "de-CH": "Rabattcodes sind e exklusive Vorteil vo dr DogiCrew 🩵. Wird jetzt Teil devo, zum sie freizschalte, sobald die erste Kooperatione live sind.", - en: "Discount codes are an exclusive perk of the DogiCrew 🩵. Join now to unlock them as soon as the first collaborations go live.", - fr: "Les codes de réduction sont un avantage exclusif de la DogiCrew 🩵. Rejoins-la dès maintenant pour les débloquer dès que les premières collaborations seront lancées.", - pt: "Os códigos de desconto são um benefício exclusivo da DogiCrew 🩵. Junte-se agora para desbloqueá-los assim que as primeiras parcerias forem lançadas.", + de: "Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Der erste Partnercode ist schon drin — werde jetzt Teil davon, um ihn freizuschalten.", + "de-CH": "Rabattcodes sind e exklusive Vorteil vo dr DogiCrew 🩵. De erst Partnercode isch scho dinn — wird jetzt Teil devo, zum en freizschalte.", + en: "Discount codes are an exclusive perk of the DogiCrew 🩵. The first partner code is already in there — join now to unlock it.", + fr: "Les codes de réduction sont un avantage exclusif de la DogiCrew 🩵. Le premier code partenaire est déjà là — rejoins-la maintenant pour le débloquer.", + pt: "Os códigos de desconto são um benefício exclusivo da DogiCrew 🩵. O primeiro código de parceiro já lá está — junta-te agora para o desbloquear.", }, rc_btn_abo: { de: "Jetzt Supporter werden 👑", "de-CH": "Jetzt Supporter werde 👑", en: "Become a supporter now 👑", fr: "Devenir Supporter maintenant 👑", pt: "Tornar-se apoiador agora 👑", }, - rc_titel: { - de: "Codes folgen in Kürze", "de-CH": "Codes chömed bald", - en: "Codes coming soon", fr: "Codes bientôt disponibles", pt: "Códigos em breve", + + /* ---- Erster Partner: Van's DIY & Bastelbedarf (DOGI10) ---------- */ + rc_band: { + de: "Partner Nr. 1", "de-CH": "Partner Nr. 1", + en: "Partner no. 1", fr: "Partenaire n° 1", pt: "Parceiro n.º 1", }, - rc_text: { - de: "Als Creator bekommt DogFather eigene Rabattcodes bei ausgewählten Partnern — die gibt er direkt an Team Dogi weiter. Sobald die ersten Kooperationen live sind, stehen die Codes genau hier.", - "de-CH": "Als Creator überchunnt DogFather eigeni Rabattcodes bi usgwählte Partner — die gitt er direkt a Team Dogi wyter. Sobald die erste Kooperatione live sind, stönd die Codes genau da.", - en: "As a creator, DogFather gets his own discount codes from selected partners — and passes them straight on to Team Dogi. As soon as the first collaborations go live, the codes will show up right here.", - fr: "En tant que créateur, DogFather reçoit ses propres codes de réduction chez des partenaires sélectionnés — et les transmet directement à Team Dogi. Dès que les premières collaborations seront lancées, les codes apparaîtront ici.", - pt: "Como criador, DogFather recebe seus próprios códigos de desconto de parceiros selecionados — e os repassa direto para a Team Dogi. Assim que as primeiras parcerias forem lançadas, os códigos aparecerão bem aqui.", + /* Attribut-Eintrag: blankes "&", siehe Kopf dieser Datei. */ + rc_logo_alt: { + de: "Logo von Van's DIY & Bastelbedarf", + "de-CH": "Logo vo Van's DIY & Bastelbedarf", + en: "Logo of Van's DIY & Bastelbedarf", + fr: "Logo de Van's DIY & Bastelbedarf", + pt: "Logótipo de Van's DIY & Bastelbedarf", }, - rc_beispiel_marke: { - de: "Partner-Marke", "de-CH": "Partner-Marke", - en: "Partner brand", fr: "Marque partenaire", pt: "Marca parceira", + /* Die aufgezählten Warengruppen sind die echten Kategorien des Shops + (src/content/categories/), nicht das, was man bei "Bastelbedarf" + vermuten würde: Bastelzubehör, Häkelzubehör, DIY Häkelwerke, DIY + Plushies, DIY Modeschmuck & Accessoires, Mystery-Pakete. */ + rc_beschreibung: { + de: "Häkelzubehör, Bastelbedarf, DIY-Plushies und Modeschmuck — VanVans eigener Shop.", + "de-CH": "Häkelzubehör, Bastelbedarf, DIY-Plushies und Modeschmuck — VanVans eigene Shop.", + en: "Crochet supplies, craft materials, DIY plushies and handmade jewellery — VanVan's own shop.", + fr: "Accessoires de crochet, matériel de bricolage, peluches DIY et bijoux — la boutique de VanVan.", + pt: "Acessórios de crochet, material de artesanato, plushies DIY e bijutaria — a loja da VanVan.", }, - rc_beispiel_code: { - de: `Code „DOGFATHER" · –10 %`, "de-CH": `Code „DOGFATHER" · –10 %`, - en: `Code "DOGFATHER" · –10%`, fr: `Code « DOGFATHER » · –10 %`, pt: `Código "DOGFATHER" · –10%`, + rc_code_kopf: { + de: "Dein Code", "de-CH": "Din Code", + en: "Your code", fr: "Ton code", pt: "O teu código", }, - rc_btn_kontakt: { - de: "Auf dem Laufenden bleiben", "de-CH": "Am Bau blibe", - en: "Stay in the loop", fr: "Rester informé(e)", pt: "Fique por dentro", + rc_code_fuss: { + de: "Tippen zum Kopieren", "de-CH": "Tippe zum Kopiere", + en: "Tap to copy", fr: "Touche pour copier", pt: "Toca para copiar", + }, + rc_code_kopiert: { + de: "Code kopiert ✓", "de-CH": "Code kopiert ✓", + en: "Code copied ✓", fr: "Code copié ✓", pt: "Código copiado ✓", + }, + rc_code_fehler: { + de: "Kopieren ging nicht — bitte von Hand abtippen.", + "de-CH": "Kopiere het nöd klappt — bitte vo Hand abtippe.", + en: "Copying didn't work — please type it in by hand.", + fr: "La copie n'a pas fonctionné — saisis-le à la main.", + pt: "A cópia não funcionou — escreve-o à mão.", + }, + /* Die drei Konditionen stehen 1:1 so im Shop (partnercodes.json, + Eintrag DOGI10) — nicht aus dem Codenamen geschlossen. */ + rc_fakt_rabatt: { + de: "10 % Rabatt auf deinen Einkauf", + "de-CH": "10 % Rabatt uf din Iikauf", + en: "10% off your order", + fr: "10 % de remise sur ta commande", + pt: "10 % de desconto na tua compra", + }, + rc_fakt_bis: { + de: "Gültig bis 30.09.2026", "de-CH": "Gültig bis 30.09.2026", + en: "Valid until 30 September 2026", fr: "Valable jusqu'au 30.09.2026", pt: "Válido até 30.09.2026", + }, + rc_fakt_sale: { + de: "Nicht auf bereits reduzierte Artikel", + "de-CH": "Nöd uf scho reduzierti Artikel", + en: "Not valid on items that are already reduced", + fr: "Non valable sur les articles déjà réduits", + pt: "Não válido em artigos já reduzidos", + }, + rc_abgelaufen: { + de: "Dieser Code ist ausgelaufen. Dogi klärt gerade die Verlängerung — schau bald wieder rein.", + "de-CH": "De Code isch abglaufe. Dogi klärt grad d Verlängerig — lueg bald wieder verbi.", + en: "This code has expired. Dogi is sorting out an extension — check back soon.", + fr: "Ce code a expiré. Dogi s'occupe d'une prolongation — repasse bientôt.", + pt: "Este código expirou. O Dogi está a tratar da renovação — volta em breve.", + }, + rc_btn_shop: { + de: "Zum Shop von Van’s DIY ↗", "de-CH": "Zum Shop vo Van’s DIY ↗", + en: "Go to the Van’s DIY shop ↗", fr: "Voir la boutique Van’s DIY ↗", pt: "Ir à loja Van’s DIY ↗", + }, + rc_mehr: { + de: "Weitere Codes folgen, sobald die nächsten Kooperationen stehen.", + "de-CH": "Wyteri Codes chömed, sobald die nächste Kooperatione stönd.", + en: "More codes will follow as soon as the next collaborations are in place.", + fr: "D'autres codes suivront dès que les prochaines collaborations seront conclues.", + pt: "Mais códigos seguem assim que as próximas parcerias estiverem fechadas.", }, rc_btn_zurueck: { de: "← Zurück zu allen Links", "de-CH": "← Zrugg zu alle Links", diff --git a/pruef-rabattcodes.mjs b/pruef-rabattcodes.mjs new file mode 100644 index 00000000..ebe1e038 --- /dev/null +++ b/pruef-rabattcodes.mjs @@ -0,0 +1,358 @@ +/* Prüft die Rabattcode-Seite mit dem ersten echten Partnercode (DOGI10). + + WAS HIER SCHIEFGEHEN KANN, UND WARUM MAN ES NICHT SIEHT + + Die Seite hat zwei Zustände, und man bekommt immer nur einen davon zu + sehen: Ohne Supporter-Abo die Sperre, mit Abo die Codekarte. Wer im + Browser nachschaut, prüft also die Hälfte. Beide Richtungen stehen + deshalb hier drin — auch die Gegenprobe, dass die Karte OHNE Abo + wirklich verschwunden ist und nicht bloss weiter unten steht. + + 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 URL = "http://127.0.0.1:4331/rabattcodes.html"; + +/* 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(); + +/* Eine Seite im gewünschten Zustand öffnen. `abo` schaltet die Sperre, + `zeit` stellt die Uhr (nur Date, die Zeitgeber laufen weiter — sonst + stünde main.js still). */ +async function seiteOeffnen({ abo = true, zeit = null, sprache = "de", viewport, reducedMotion } = {}) { + 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(([a, s]) => { + if (a) localStorage.setItem("supporterSubStatus", "active"); + else localStorage.removeItem("supporterSubStatus"); + localStorage.setItem("dogi-sprache", s); + }, [abo, sprache]); + await p.goto(URL, { waitUntil: "domcontentloaded" }); + await p.waitForTimeout(700); + return { ctx, p }; +} + +/* =================================================================== + 1 — Die Sperre, in BEIDE Richtungen + =================================================================== */ +console.log("\n=== Supporter-Sperre ==="); +for (const [abo, sichtbar] of [[true, "Karte"], [false, "Sperre"]]) { + const { ctx, p } = await seiteOeffnen({ abo }); + const z = await p.evaluate(() => ({ + karte: !document.getElementById("rc-inhalt").hidden, + sperre: !document.getElementById("rc-gesperrt").hidden, + codeSichtbar: !!document.getElementById("rc-code")?.checkVisibility?.(), + })); + const lage = abo ? "Mit Abo" : "Ohne Abo"; + pruefe(`${lage}: ${sichtbar} steht da`, + z.karte === abo && z.sperre === !abo, `Karte=${z.karte} Sperre=${z.sperre}`); + /* Gegenprobe: "hidden" ist ein Attribut, kein Beweis. Ohne Abo darf + der Code auch tatsächlich nicht auf dem Schirm sein. */ + pruefe(`${lage}: Code ist ${abo ? "sichtbar" : "unsichtbar"}`, + z.codeSichtbar === abo, `sichtbar=${z.codeSichtbar}`); + 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, "&" 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({ abo: false }); + 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 = 34; +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."); diff --git a/rabattcodes.html b/rabattcodes.html index 560cdba4..1cc054da 100644 --- a/rabattcodes.html +++ b/rabattcodes.html @@ -25,50 +25,11 @@ @@ -103,25 +401,54 @@
-