Analytics: Banner-Skript Review-Fixes (R8/R9, Tab-Widerruf)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 09:53:59 +00:00
1 parent 5930f052d7
commit da8bba2a1c
6 files changed
+233 -146

No files matched your search

+15 -25
View File
@@ -13,9 +13,12 @@ export function vpClass(width) {
export function cleanPath(href) { export function cleanPath(href) {
let p = '/'; let p = '/';
try { const h = String(href == null ? '' : href);
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/'; if (h.charAt(0) === '/') {
} catch (e) { /* bleibt '/' */ } p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p; if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300); return p.slice(0, 300);
@@ -34,32 +37,19 @@ export function refHost(referrer, ownHost) {
} catch (e) { return ''; } } catch (e) { return ''; }
} }
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen. // Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
export function cssPath(el) { export function cssPath(el) {
const parts = []; const parts = [];
let n = el; let n = el;
while (n && n.tagName && parts.length < 5) { while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase(); const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
let seg = tag; if (!tag) break;
const id = n.id ? safeName(n.id) : ''; let idx = 1;
if (id && !looksGenerated(id)) { for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
parts.unshift(seg + '#' + id); if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
break;
} }
const raw = n.getAttribute ? n.getAttribute('class') : ''; parts.unshift(tag + ':nth-of-type(' + idx + ')');
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2); if (tag === 'body' || tag === 'html') break;
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode; n = n.parentNode;
} }
let out = parts.join(' > '); let out = parts.join(' > ');
+18 -28
View File
@@ -16,9 +16,12 @@ function vpClass(width) {
function cleanPath(href) { function cleanPath(href) {
let p = '/'; let p = '/';
try { const h = String(href == null ? '' : href);
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/'; if (h.charAt(0) === '/') {
} catch (e) { /* bleibt '/' */ } p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p; if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300); return p.slice(0, 300);
@@ -37,32 +40,19 @@ function refHost(referrer, ownHost) {
} catch (e) { return ''; } } catch (e) { return ''; }
} }
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen. // Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
function cssPath(el) { function cssPath(el) {
const parts = []; const parts = [];
let n = el; let n = el;
while (n && n.tagName && parts.length < 5) { while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase(); const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
let seg = tag; if (!tag) break;
const id = n.id ? safeName(n.id) : ''; let idx = 1;
if (id && !looksGenerated(id)) { for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
parts.unshift(seg + '#' + id); if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
break;
} }
const raw = n.getAttribute ? n.getAttribute('class') : ''; parts.unshift(tag + ':nth-of-type(' + idx + ')');
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2); if (tag === 'body' || tag === 'html') break;
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode; n = n.parentNode;
} }
let out = parts.join(' > '); let out = parts.join(' > ');
@@ -140,7 +130,7 @@ function main() {
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.className = 'bar'; bar.className = 'bar';
const msg = document.createElement('span'); const msg = document.createElement('span');
msg.innerHTML = escapeHtml('Heatmap wird geladen ...'); msg.textContent = 'Heatmap wird geladen ...';
const close = document.createElement('button'); const close = document.createElement('button');
close.setAttribute('type', 'button'); close.setAttribute('type', 'button');
close.textContent = 'Schließen'; close.textContent = 'Schließen';
@@ -212,11 +202,11 @@ function main() {
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); }) .then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) { .then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; }); points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)'); msg.textContent = 'Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)';
draw(); draw();
timer = setInterval(redrawIfChanged, 1000); timer = setInterval(redrawIfChanged, 1000);
}) })
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); }); .catch(function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
} }
try { main(); } catch (e) { /* nie die Seite stoeren */ } try { main(); } catch (e) { /* nie die Seite stoeren */ }
+53 -45
View File
@@ -16,9 +16,12 @@ function vpClass(width) {
function cleanPath(href) { function cleanPath(href) {
let p = '/'; let p = '/';
try { const h = String(href == null ? '' : href);
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/'; if (h.charAt(0) === '/') {
} catch (e) { /* bleibt '/' */ } p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p; if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300); return p.slice(0, 300);
@@ -37,32 +40,19 @@ function refHost(referrer, ownHost) {
} catch (e) { return ''; } } catch (e) { return ''; }
} }
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen. // Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
function cssPath(el) { function cssPath(el) {
const parts = []; const parts = [];
let n = el; let n = el;
while (n && n.tagName && parts.length < 5) { while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase(); const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
let seg = tag; if (!tag) break;
const id = n.id ? safeName(n.id) : ''; let idx = 1;
if (id && !looksGenerated(id)) { for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
parts.unshift(seg + '#' + id); if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
break;
} }
const raw = n.getAttribute ? n.getAttribute('class') : ''; parts.unshift(tag + ':nth-of-type(' + idx + ')');
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2); if (tag === 'body' || tag === 'html') break;
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode; n = n.parentNode;
} }
let out = parts.join(' > '); let out = parts.join(' > ');
@@ -128,8 +118,10 @@ function escapeHtml(s) {
function main() { function main() {
const DOC = document; const DOC = document;
const WIN = window; const WIN = window;
if (WIN.__wkAnalytics) return; // doppelt eingebunden
WIN.__wkAnalytics = true;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site') const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src$="/a/w.js"]'); ? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
if (!SELF) return; if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || ''); const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return; if (!/^[a-z0-9]{16,40}$/.test(site)) return;
@@ -184,6 +176,7 @@ function main() {
}), 300); }), 300);
} }
sendPv(true); sendPv(true);
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
try { try {
const orig = WIN.history.pushState; const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; }; WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
@@ -191,6 +184,9 @@ function main() {
} catch (e) { /* egal */ } } catch (e) { /* egal */ }
// ---- Tracking (nur nach Annehmen erreichbar) ---- // ---- Tracking (nur nach Annehmen erreichbar) ----
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
let memChoice = null;
let tracking = false; let tracking = false;
const hosts = []; const hosts = [];
const scrollPct = () => { const scrollPct = () => {
@@ -200,6 +196,7 @@ function main() {
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000)); const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) { const onClick = safe(function (e) {
if (!allowed()) return;
const t = e.target; const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return; if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight; const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
@@ -218,13 +215,21 @@ function main() {
}); });
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); }); const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
const onLeave = safe(function () { const onLeave = safe(function () {
if (enSent) return; if (enSent || !allowed()) return;
enSent = true; enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(), send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) }); sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
}); });
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); }); const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
function allowed() {
const c = readConsent(storage, Date.now());
const ok = c ? c.a === 'accept' : !!(memChoice && memChoice.a === 'accept');
if (!ok && tracking) stopTracking();
return ok;
}
function startTracking() { function startTracking() {
if (tracking) return; if (tracking) return;
tracking = true; tracking = true;
@@ -242,17 +247,17 @@ function main() {
} }
// ---- Einwilligung ---- // ---- Einwilligung ----
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1'; const dnt = (v) => v === '1' || v === 'yes';
const gpc = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const stored = readConsent(storage, Date.now()); const stored = readConsent(storage, Date.now());
let bannerHost = null; let bannerHost = null;
let linkHostEl = null; let linkHostEl = null;
function choose(a) { function choose(a) {
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ } memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a }); send({ t: 'bn', site, a });
if (a === 'accept') startTracking(); else stopTracking(); if (a === 'accept') startTracking(); else stopTracking();
closeBanner(); closeBanner();
@@ -271,12 +276,11 @@ function main() {
'.t{margin:0;max-width:720px}' + '.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' + '.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' + '.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' + 'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' + 'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}'; '@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
function openBanner() { function openBanner(focus) {
if (bannerHost) return; if (bannerHost) return;
const prev = DOC.activeElement; const prev = DOC.activeElement;
bannerHost = DOC.createElement('div'); bannerHost = DOC.createElement('div');
@@ -286,12 +290,11 @@ function main() {
const root = bannerHost.attachShadow({ mode: 'open' }); const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS)); root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' }); const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der '); const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details: ');
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info')); p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz'));
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
const row = el('div', { class: 'r' }); const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen'); const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen'); const yes = el('button', { type: 'button' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); })); no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); })); yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes); row.appendChild(no); row.appendChild(yes);
@@ -299,7 +302,7 @@ function main() {
root.appendChild(bar); root.appendChild(bar);
DOC.body.appendChild(bannerHost); DOC.body.appendChild(bannerHost);
if (linkHostEl) linkHostEl.style.display = 'none'; if (linkHostEl) linkHostEl.style.display = 'none';
no.focus(); if (focus === true) no.focus();
} }
function closeBanner() { function closeBanner() {
if (!bannerHost) return; if (!bannerHost) return;
@@ -318,19 +321,24 @@ function main() {
const root = linkHostEl.attachShadow({ mode: 'open' }); const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}')); root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen'); const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
b.addEventListener('click', safe(openBanner)); b.addEventListener('click', safe(function () { openBanner(true); }));
root.appendChild(b); root.appendChild(b);
DOC.body.appendChild(linkHostEl); DOC.body.appendChild(linkHostEl);
} }
if (DOC.querySelector('[data-webklar-consent]')) {
DOC.addEventListener('click', safe(function (e) { DOC.addEventListener('click', safe(function (e) {
const t = e.target; const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(); } if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
}));
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now());
if (c && c.a === 'accept') startTracking(); else stopTracking();
if (c) closeBanner();
})); }));
} else {
addSettingsLink();
}
if (stored) { if (stored) {
if (stored.a === 'accept') startTracking(); if (stored.a === 'accept') startTracking();
+3 -4
View File
@@ -1,5 +1,4 @@
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen). // WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
import { escapeHtml } from './core.js';
function main() { function main() {
const me = document.currentScript; const me = document.currentScript;
@@ -17,7 +16,7 @@ function main() {
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.className = 'bar'; bar.className = 'bar';
const msg = document.createElement('span'); const msg = document.createElement('span');
msg.innerHTML = escapeHtml('Heatmap wird geladen ...'); msg.textContent = 'Heatmap wird geladen ...';
const close = document.createElement('button'); const close = document.createElement('button');
close.setAttribute('type', 'button'); close.setAttribute('type', 'button');
close.textContent = 'Schließen'; close.textContent = 'Schließen';
@@ -89,11 +88,11 @@ function main() {
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); }) .then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) { .then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; }); points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)'); msg.textContent = 'Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)';
draw(); draw();
timer = setInterval(redrawIfChanged, 1000); timer = setInterval(redrawIfChanged, 1000);
}) })
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); }); .catch(function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
} }
try { main(); } catch (e) { /* nie die Seite stoeren */ } try { main(); } catch (e) { /* nie die Seite stoeren */ }
+38 -20
View File
@@ -5,8 +5,10 @@ import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsen
function main() { function main() {
const DOC = document; const DOC = document;
const WIN = window; const WIN = window;
if (WIN.__wkAnalytics) return; // doppelt eingebunden
WIN.__wkAnalytics = true;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site') const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src$="/a/w.js"]'); ? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
if (!SELF) return; if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || ''); const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return; if (!/^[a-z0-9]{16,40}$/.test(site)) return;
@@ -61,6 +63,7 @@ function main() {
}), 300); }), 300);
} }
sendPv(true); sendPv(true);
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
try { try {
const orig = WIN.history.pushState; const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; }; WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
@@ -68,6 +71,9 @@ function main() {
} catch (e) { /* egal */ } } catch (e) { /* egal */ }
// ---- Tracking (nur nach Annehmen erreichbar) ---- // ---- Tracking (nur nach Annehmen erreichbar) ----
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
let memChoice = null;
let tracking = false; let tracking = false;
const hosts = []; const hosts = [];
const scrollPct = () => { const scrollPct = () => {
@@ -77,6 +83,7 @@ function main() {
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000)); const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) { const onClick = safe(function (e) {
if (!allowed()) return;
const t = e.target; const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return; if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight; const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
@@ -95,13 +102,21 @@ function main() {
}); });
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); }); const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
const onLeave = safe(function () { const onLeave = safe(function () {
if (enSent) return; if (enSent || !allowed()) return;
enSent = true; enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(), send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) }); sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
}); });
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); }); const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
function allowed() {
const c = readConsent(storage, Date.now());
const ok = c ? c.a === 'accept' : !!(memChoice && memChoice.a === 'accept');
if (!ok && tracking) stopTracking();
return ok;
}
function startTracking() { function startTracking() {
if (tracking) return; if (tracking) return;
tracking = true; tracking = true;
@@ -119,17 +134,17 @@ function main() {
} }
// ---- Einwilligung ---- // ---- Einwilligung ----
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1'; const dnt = (v) => v === '1' || v === 'yes';
const gpc = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const stored = readConsent(storage, Date.now()); const stored = readConsent(storage, Date.now());
let bannerHost = null; let bannerHost = null;
let linkHostEl = null; let linkHostEl = null;
function choose(a) { function choose(a) {
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ } memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a }); send({ t: 'bn', site, a });
if (a === 'accept') startTracking(); else stopTracking(); if (a === 'accept') startTracking(); else stopTracking();
closeBanner(); closeBanner();
@@ -148,12 +163,11 @@ function main() {
'.t{margin:0;max-width:720px}' + '.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' + '.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' + '.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' + 'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' + 'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}'; '@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
function openBanner() { function openBanner(focus) {
if (bannerHost) return; if (bannerHost) return;
const prev = DOC.activeElement; const prev = DOC.activeElement;
bannerHost = DOC.createElement('div'); bannerHost = DOC.createElement('div');
@@ -163,12 +177,11 @@ function main() {
const root = bannerHost.attachShadow({ mode: 'open' }); const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS)); root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' }); const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der '); const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details: ');
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info')); p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz'));
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
const row = el('div', { class: 'r' }); const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen'); const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen'); const yes = el('button', { type: 'button' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); })); no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); })); yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes); row.appendChild(no); row.appendChild(yes);
@@ -176,7 +189,7 @@ function main() {
root.appendChild(bar); root.appendChild(bar);
DOC.body.appendChild(bannerHost); DOC.body.appendChild(bannerHost);
if (linkHostEl) linkHostEl.style.display = 'none'; if (linkHostEl) linkHostEl.style.display = 'none';
no.focus(); if (focus === true) no.focus();
} }
function closeBanner() { function closeBanner() {
if (!bannerHost) return; if (!bannerHost) return;
@@ -195,19 +208,24 @@ function main() {
const root = linkHostEl.attachShadow({ mode: 'open' }); const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}')); root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen'); const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
b.addEventListener('click', safe(openBanner)); b.addEventListener('click', safe(function () { openBanner(true); }));
root.appendChild(b); root.appendChild(b);
DOC.body.appendChild(linkHostEl); DOC.body.appendChild(linkHostEl);
} }
if (DOC.querySelector('[data-webklar-consent]')) {
DOC.addEventListener('click', safe(function (e) { DOC.addEventListener('click', safe(function (e) {
const t = e.target; const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(); } if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
}));
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now());
if (c && c.a === 'accept') startTracking(); else stopTracking();
if (c) closeBanner();
})); }));
} else {
addSettingsLink();
}
if (stored) { if (stored) {
if (stored.a === 'accept') startTracking(); if (stored.a === 'accept') startTracking();
+106 -24
View File
@@ -29,6 +29,8 @@ test('cleanPath', () => {
assert.equal(cleanPath(''), '/'); assert.equal(cleanPath(''), '/');
assert.equal(cleanPath('/' + 'a'.repeat(400)).length, 300); assert.equal(cleanPath('/' + 'a'.repeat(400)).length, 300);
assert.equal(cleanPath(null), '/'); assert.equal(cleanPath(null), '/');
assert.equal(cleanPath('//foo/bar?x=1'), '//foo/bar');
assert.equal(cleanPath('//foo'), '//foo');
}); });
test('refHost', () => { test('refHost', () => {
@@ -42,30 +44,33 @@ test('refHost', () => {
}); });
function el(tag, o = {}) { function el(tag, o = {}) {
const e = { tagName: tag.toUpperCase(), id: o.id || '', parentNode: o.parent || null, attrs: { class: o.cls || '' }, const e = { tagName: tag.toUpperCase(), id: o.id || '', parentNode: o.parent || null, previousElementSibling: o.prev || null,
getAttribute(n) { return this.attrs[n] ?? null; }, classList: (o.cls || '').split(/\s+/).filter(Boolean) }; attrs: { class: o.cls || '' }, getAttribute(n) { return this.attrs[n] ?? null; } };
return e; return e;
} }
test('cssPath', () => { test('cssPath: nur Tags + nth-of-type, keine IDs/Klassen', () => {
const body = el('body'); const body = el('body');
const div = el('div', { parent: body, cls: 'hero main' }); const div = el('div', { parent: body, cls: 'hero main' });
const btn = el('button', { parent: div, cls: 'cta' }); const p1 = el('p', { parent: div });
assert.equal(cssPath(btn), 'body > div.hero.main > button.cta'); const btn0 = el('button', { parent: div, prev: p1 });
const withId = el('section', { parent: body, id: 'kontakt' }); const btn = el('button', { parent: div, prev: btn0, cls: 'cta' });
const a = el('a', { parent: withId }); assert.equal(cssPath(btn), 'body:nth-of-type(1) > div:nth-of-type(1) > button:nth-of-type(2)');
assert.equal(cssPath(a), 'section#kontakt > a'); const pii = el('a', { parent: body, id: 'max@firma.de', cls: 'team-max-mustermann' });
// generierte IDs/Klassen werden uebersprungen const sel = cssPath(pii);
const gen = el('div', { parent: body, id: 'ember12345', cls: 'css-1a2b3c x9f8e7d6c5b4a ok' }); assert.equal(sel, 'body:nth-of-type(1) > a:nth-of-type(1)');
assert.equal(cssPath(gen), 'body > div.ok'); assert.ok(!/max|firma|mustermann|#|\./.test(sel));
// max. 5 Ebenen // max. 6 Ebenen
let p = el('div'); const first = p; let n = el('div');
for (let i = 0; i < 9; i++) p = el('span', { parent: p }); for (let i = 0; i < 9; i++) n = el('span', { parent: n });
assert.equal(cssPath(p).split(' > ').length, 5); assert.equal(cssPath(n).split(' > ').length, 6);
assert.ok(first); // stoppt bei body
const outer = el('html'); body.parentNode = outer;
assert.equal(cssPath(div).split(' > ')[0], 'body:nth-of-type(1)');
// <= 200 // <= 200
const long = el('div', { parent: body, cls: Array.from({ length: 40 }, (_, i) => 'klasse' + 'x'.repeat(i % 5 + 3)).join(' ') }); let deep = el('div');
assert.ok(cssPath(long).length <= 200); for (let i = 0; i < 5; i++) deep = el('x'.repeat(40), { parent: deep });
assert.ok(cssPath(deep).length <= 200);
assert.equal(cssPath(null), ''); assert.equal(cssPath(null), '');
}); });
@@ -198,7 +203,9 @@ function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar
const types = () => sent.map((s) => s.d.t + (s.d.a ? ':' + s.d.a : '')); const types = () => sent.map((s) => s.d.t + (s.d.a ? ':' + s.d.a : ''));
const ck = (target, ev = {}) => { for (const f of [...(docL.click || [])]) f({ type: 'click', target, pageX: 500, pageY: 1000, clientX: 500, clientY: 700, preventDefault() {}, ...ev }); }; const ck = (target, ev = {}) => { for (const f of [...(docL.click || [])]) f({ type: 'click', target, pageX: 500, pageY: 1000, clientX: 500, clientY: 700, preventDefault() {}, ...ev }); };
const leave = () => { for (const f of [...(winL.pagehide || [])]) f({}); document.visibilityState = 'hidden'; for (const f of [...(docL.visibilitychange || [])]) f({}); }; const leave = () => { for (const f of [...(winL.pagehide || [])]) f({}); document.visibilityState = 'hidden'; for (const f of [...(docL.visibilitychange || [])]) f({}); };
return { sent, store, types, ck, leave, body, docL, winL, win, injected, history, timers, setNow: (n) => { now = n; }, link, document }; const rerun = () => vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
const storage = () => (winL.storage || []).forEach((f) => f({ key: 'wk-consent' }));
return { rerun, storage, sent, store, types, ck, leave, body, docL, winL, win, injected, history, timers, setNow: (n) => { now = n; }, link, document };
} }
const target = () => { const b = makeEl('button'); b.parentNode = makeEl('body'); b.attrs.class = 'cta'; return b; }; const target = () => { const b = makeEl('button'); b.parentNode = makeEl('body'); b.attrs.class = 'cta'; return b; };
const hostEl = (h) => h.body.children.find((c) => c.shadow && findAll(c.shadow, (n) => n.attrs && n.attrs.role === 'dialog').length); const hostEl = (h) => h.body.children.find((c) => c.shadow && findAll(c.shadow, (n) => n.attrs && n.attrs.role === 'dialog').length);
@@ -211,7 +218,7 @@ test('w.js: ohne Einwilligung nur pv + bn shown, keine ck/ct/en', () => {
assert.deepEqual(h.sent[1].d, { t: 'bn', site: 'abcdef0123456789abcd', a: 'shown' }); assert.deepEqual(h.sent[1].d, { t: 'bn', site: 'abcdef0123456789abcd', a: 'shown' });
h.ck(target()); h.leave(); h.ck(target()); h.leave();
assert.deepEqual(h.types(), ['pv', 'bn:shown']); assert.deepEqual(h.types(), ['pv', 'bn:shown']);
assert.equal((h.docL.click || []).length, 0); assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
}); });
test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)', () => { test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)', () => {
@@ -225,13 +232,16 @@ test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)'
assert.match(text, /Wir möchten mit deiner Zustimmung anonym auswerten/); assert.match(text, /Wir möchten mit deiner Zustimmung anonym auswerten/);
const a = all.find((n) => n.tagName === 'A'); const a = all.find((n) => n.tagName === 'A');
assert.equal(a.attrs.href, '/datenschutz-analyse.html'); assert.equal(a.attrs.href, '/datenschutz-analyse.html');
assert.equal(a.textContent, 'Datenschutz-Info'); assert.equal(a.textContent, 'Datenschutz');
const css = all.filter((n) => n.tagName === 'STYLE').map((n) => n.textContent).join(''); const css = all.filter((n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
assert.match(css, /#0a5a8a/); assert.match(css, /#0a5a8a/);
assert.match(css, /max-width:\s*767px/); assert.match(css, /max-width:\s*767px/);
const buttons = all.filter((n) => n.tagName === 'BUTTON'); const buttons = all.filter((n) => n.tagName === 'BUTTON');
assert.deepEqual(buttons.map((b) => b.textContent), ['Ablehnen', 'Annehmen']); assert.deepEqual(buttons.map((b) => b.textContent), ['Ablehnen', 'Annehmen']);
assert.ok(buttons[0].focused, 'erster Button fokussiert'); assert.ok(!buttons[0].focused, 'kein Fokusraub beim Laden');
assert.equal(buttons[0].attrs.class, buttons[1].attrs.class);
assert.ok(!/button\.|\.p\b/.test(css), 'eine Regel fuer beide Buttons');
assert.match(css, /button\{[^}]*background:#0a5a8a[^}]*color:#fff/);
assert.equal(host.style.zIndex, '2147483647'); assert.equal(host.style.zIndex, '2147483647');
// Escape schliesst nicht // Escape schliesst nicht
fire(dlg, 'keydown', { key: 'Escape' }); fire(host, 'keydown', { key: 'Escape' }); fire(dlg, 'keydown', { key: 'Escape' }); fire(host, 'keydown', { key: 'Escape' });
@@ -248,7 +258,7 @@ test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort',
const t = target(); const t = target();
h.ck(t); h.ck(t);
const ck = h.sent.at(-1).d; const ck = h.sent.at(-1).d;
assert.deepEqual(ck, { t: 'ck', site: 'abcdef0123456789abcd', path: '/start', vp: 'd', x: 0.5, y: 0.5, sel: 'body > button.cta' }); assert.deepEqual(ck, { t: 'ck', site: 'abcdef0123456789abcd', path: '/start', vp: 'd', x: 0.5, y: 0.5, sel: 'body:nth-of-type(1) > button:nth-of-type(1)' });
// Kontakt-Link: ck + ct (Kind-Element in <a>) // Kontakt-Link: ck + ct (Kind-Element in <a>)
const a = makeEl('a'); a.setAttribute('href', 'tel:+4912'); a.parentNode = makeEl('body'); const a = makeEl('a'); a.setAttribute('href', 'tel:+4912'); a.parentNode = makeEl('body');
const span = makeEl('span'); span.parentNode = a; const span = makeEl('span'); span.parentNode = a;
@@ -268,12 +278,13 @@ test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort',
const settings = findAll(h.body, (x) => x.tagName === 'BUTTON' && x.textContent === 'Cookie-Einstellungen')[0]; const settings = findAll(h.body, (x) => x.tagName === 'BUTTON' && x.textContent === 'Cookie-Einstellungen')[0];
assert.ok(settings, 'fester Link'); assert.ok(settings, 'fester Link');
settings.click(); settings.click();
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
btn(hostEl(h).shadow, 'Ablehnen').click(); btn(hostEl(h).shadow, 'Ablehnen').click();
assert.equal(h.types().at(-1), 'bn:reject'); assert.equal(h.types().at(-1), 'bn:reject');
const before = h.sent.length; const before = h.sent.length;
h.ck(target()); h.leave(); h.ck(target()); h.leave();
assert.equal(h.sent.length, before); assert.equal(h.sent.length, before);
assert.equal((h.docL.click || []).length, 0); assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
assert.equal((h.docL.visibilitychange || []).length, 0); assert.equal((h.docL.visibilitychange || []).length, 0);
assert.equal((h.winL.pagehide || []).length, 0); assert.equal((h.winL.pagehide || []).length, 0);
}); });
@@ -343,3 +354,74 @@ test('w.js: wirft nie, auch wenn localStorage/sendBeacon kaputt', () => {
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox); vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
}); });
}); });
test('w.js: DNT yes / msDoNotTrack => nur pv', () => {
for (const o of [{ dnt: 'yes' }]) { const h = harness(o); assert.deepEqual(h.types(), ['pv']); }
});
test('w.js: doppelt eingebunden => nur ein pv', () => {
const h = harness({ gpc: true });
h.rerun();
assert.deepEqual(h.types(), ['pv']);
});
test('w.js: Widerruf in anderem Tab (storage-Ereignis) stoppt Tracking sofort', () => {
const h = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 } });
h.ck(target());
assert.equal(h.sent.at(-1).d.t, 'ck');
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', t: 1_700_000_000_000 });
h.storage();
const n = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, n);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
// Annehmen in anderem Tab startet Tracking
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'accept', t: 1_700_000_000_000 });
h.storage();
h.ck(target());
assert.equal(h.sent.length, n + 1);
});
test('w.js: Einwilligung wird vor jedem Senden neu geprueft (ohne storage-Ereignis)', () => {
const h = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 } });
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', t: 1_700_000_000_000 });
const n = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, n);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
});
test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () => {
const h = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 } });
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.map((s) => s.d.path), ['/start', '/zwei']);
h.ck(target());
assert.equal(h.sent.at(-1).d.path, '/zwei');
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', t: 1_700_000_000_000 });
h.storage();
const n = h.sent.length;
h.leave();
assert.equal(h.sent.length, n);
});
test('w.js: bfcache-Rueckkehr sendet neues pv und erlaubt neues en', () => {
const h = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 } });
h.leave();
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 1);
for (const f of h.winL.pageshow || []) f({ persisted: false });
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
for (const f of h.winL.pageshow || []) f({ persisted: true });
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 2);
h.leave();
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 2);
});
test('w.js: [data-webklar-consent] spaeter hinzugefuegt funktioniert per Delegation', () => {
const h = harness({ stored: { v: 1, a: 'reject', t: 1_700_000_000_000 } });
// kein Link beim Laden => fester Link; spaeteres Element oeffnet ebenfalls
const late = makeEl('a'); late.setAttribute('data-webklar-consent', '');
for (const f of [...(h.docL.click || [])]) f({ type: 'click', target: late, preventDefault() {} });
assert.ok(hostEl(h));
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused);
});