Analyse: Client prüft Einwilligung vor dem Senden, Fassung der Banner-Wahl, fetch ohne Cookies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:26 +00:00
1 parent c2cbc4156f
commit 8d64a18d35
5 files changed
+354 -131

No files matched your search

+186 -63
View File
@@ -7,8 +7,10 @@ import { dirname, join } from 'node:path';
import {
vpClass, cleanPath, refHost, cssPath, contactKind, readConsent, contrastOk, escapeHtml,
} from '../client/core.js';
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from '../client/core.js';
import { buildAll } from '../client/build.js';
const BV = '2026-10-02';
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
// ---------- core ----------
@@ -96,13 +98,17 @@ test('readConsent', () => {
const now = 1_700_000_000_000;
const day = 86400000;
const st = (v) => ({ getItem: () => v });
assert.deepEqual(readConsent(st(JSON.stringify({ v: 1, a: 'accept', t: now - 10 * day })), now), { v: 1, a: 'accept', t: now - 10 * day });
assert.equal(readConsent(st(JSON.stringify({ v: 1, a: 'reject', t: now - 182 * day })), now).a, 'reject');
assert.equal(readConsent(st(JSON.stringify({ v: 1, a: 'accept', t: now - 183 * day })), now), null);
assert.equal(readConsent(st(JSON.stringify({ v: 2, a: 'accept', t: now })), now), null);
assert.equal(readConsent(st(JSON.stringify({ v: 1, a: 'maybe', t: now })), now), null);
assert.equal(readConsent(st(JSON.stringify({ v: 1, a: 'accept', t: 'x' })), now), null);
assert.equal(readConsent(st(JSON.stringify({ v: 1, a: 'accept', t: now + day })), now), null);
const J = (o) => st(JSON.stringify(o));
assert.equal(BANNER_VERSION, '2026-10-02');
assert.deepEqual(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 10 * day }), now), { v: 1, a: 'accept', t: now - 10 * day, bv: BV });
assert.equal(readConsent(J({ v: 1, a: 'reject', bv: BV, t: now - 182 * day }), now).a, 'reject');
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 183 * day }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', t: now }), now), null, 'ohne bv');
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: '2026-01-01', t: now }), now), null, 'alte Fassung');
assert.equal(readConsent(J({ v: 2, a: 'accept', bv: BV, t: now }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'maybe', bv: BV, t: now }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: 'x' }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now + day }), now), null);
assert.equal(readConsent(st('kaputt{'), now), null);
assert.equal(readConsent(st(null), now), null);
assert.equal(readConsent({ getItem() { throw new Error('blocked'); } }, now), null);
@@ -144,6 +150,7 @@ function makeEl(tag) {
addEventListener(t, f) { (this.listeners[t] ||= []).push(f); },
removeEventListener(t, f) { this.listeners[t] = (this.listeners[t] || []).filter((x) => x !== f); },
focus() { this.focused = true; },
getBoundingClientRect() { return { height: this.fakeHeight || 0 }; },
remove() { this.removed = true; if (this.parentNode) this.parentNode.children = this.parentNode.children.filter((c) => c !== this); },
closest(sel) {
const attr = /^\[([\w-]+)\]$/.exec(sel);
@@ -162,10 +169,10 @@ function walk(n, fn) { fn(n); for (const c of n.children || []) walk(c, fn); if
function findAll(root, pred) { const out = []; walk(root, (n) => { if (pred(n)) out.push(n); }); return out; }
const btn = (root, label) => findAll(root, (n) => n.tagName === 'BUTTON' && n.textContent === label)[0];
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false } = {}) {
const sent = []; const store = {}; let now = 1_700_000_000_000;
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad } = {}) {
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
if (stored) store['wk-consent'] = JSON.stringify(stored);
const body = makeEl('body'); const html = makeEl('html');
const body = makeEl('body'); if (bodyPad !== undefined) body.style.paddingBottom = bodyPad; const html = makeEl('html');
const timers = [];
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); script.src = src;
const docL = {}; const winL = {};
@@ -173,28 +180,29 @@ function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar
const injected = [];
if (link) link.click = () => { for (const f of [...(docL.click || [])]) f({ type: 'click', target: link, preventDefault() {} }); };
const document = {
currentScript: script, referrer: 'https://www.google.com/', visibilityState: 'visible',
currentScript: script, get referrer() { reads.referrer++; return 'https://www.google.com/'; }, visibilityState: 'visible',
documentElement: Object.assign(html, { scrollWidth: 1000, scrollHeight: 2000 }), body, head: makeEl('head'),
createElement: makeEl, activeElement: null,
createElement(t) { const e = makeEl(t); if (bannerHeight) e.offsetHeight = bannerHeight; return e; }, activeElement: null,
querySelector(s) { return s === '[data-webklar-consent]' ? link : null; },
addEventListener(t, f) { (docL[t] ||= []).push(f); },
removeEventListener(t, f) { docL[t] = (docL[t] || []).filter((x) => x !== f); },
};
body.appendChild = function (c) { c.parentNode = this; this.children.push(c); if (c.attrs.src) injected.push(c); return c; };
document.head.appendChild = body.appendChild;
const history = { pushState() {}, replaceState(s, t, u) { this.replaced = u; } };
const origPush = function () {};
const history = { pushState: origPush, replaceState(s, t, u) { this.replaced = u; } };
const win = {
innerWidth: 1200, innerHeight: 800, scrollX: 0, scrollY: 300, document, history,
get innerWidth() { reads.innerWidth++; return 1200; }, innerHeight: 800, scrollX: 0, scrollY: 300, document, history,
location: { hostname: 'mein.de', origin: 'https://mein.de', pathname: '/start', search, href: 'https://mein.de/start' + search, hash: '' },
addEventListener(t, f) { (winL[t] ||= []).push(f); },
removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x !== f); },
};
const sandbox = {
window: win, document, history, location: win.location, URL, URLSearchParams, JSON, Math, Date: { now: () => now }, String, Number, Array, Object, RegExp, Error, parseInt,
navigator: { globalPrivacyControl: gpc, doNotTrack: dnt, sendBeacon(u, d) { sent.push({ u, d: JSON.parse(d) }); return true; } },
navigator: { globalPrivacyControl: gpc, doNotTrack: dnt, sendBeacon(u, d) { beacon.push({ u, d }); return true; } },
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
setTimeout: (f, ms) => { timers.push(f); return timers.length; }, clearTimeout() {},
setInterval: () => 0, clearInterval() {}, Blob: class {}, fetch: () => Promise.resolve(),
setInterval: () => 0, clearInterval() {}, Blob: class {}, fetch: (u, o) => { fetches.push({ u, o }); sent.push({ u, d: JSON.parse(o.body) }); return Promise.resolve(); },
};
win.localStorage = sandbox.localStorage;
sandbox.globalThis = sandbox;
@@ -205,31 +213,46 @@ function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar
const leave = () => { for (const f of [...(winL.pagehide || [])]) f({}); document.visibilityState = 'hidden'; for (const f of [...(docL.visibilitychange || [])]) f({}); };
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 nav = (p) => { win.location.pathname = p; history.pushState(null, '', p); timers.splice(0).forEach((f) => f()); };
return { nav, reads, beacon, fetches, origPush, 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 hostEl = (h) => h.body.children.find((c) => c.shadow && findAll(c.shadow, (n) => n.attrs && n.attrs.role === 'dialog').length);
test('w.js: ohne Einwilligung nur pv + bn shown, keine ck/ct/en', () => {
const SITE = 'abcdef0123456789abcd';
const ACC0 = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
test('w.js: ohne Einwilligung nur pv {t,site,path} + bn shown, kein Lesen von referrer/innerWidth', () => {
const h = harness();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
assert.equal(h.sent[0].u, 'https://project.webklar.com/a/e');
assert.deepEqual(h.sent[0].d, { t: 'pv', site: 'abcdef0123456789abcd', path: '/start', ref: 'www.google.com', vp: 'd' });
assert.deepEqual(h.sent[1].d, { t: 'bn', site: 'abcdef0123456789abcd', a: 'shown' });
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start' });
assert.deepEqual(Object.keys(h.sent[0].d).sort(), ['path', 'site', 't']);
assert.deepEqual(h.sent[1].d, { t: 'bn', site: SITE, a: 'shown' });
assert.equal(h.reads.referrer, 0);
assert.equal(h.reads.innerWidth, 0);
h.ck(target()); h.leave();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
});
test('core: Banner-Konstanten', () => {
assert.match(BANNER_TEXT, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
assert.match(BANNER_TEXT, /Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast\./);
assert.match(BANNER_TEXT, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
assert.equal(BANNER_LINK_LABEL, 'Datenschutz');
assert.equal(BANNER_ARIA, 'Einwilligung zur Reichweitenmessung');
assert.equal(SETTINGS_LABEL, 'Datenschutz-Einstellungen');
});
test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)', () => {
const h = harness();
const host = hostEl(h);
assert.ok(host, 'Banner im Shadow Root');
const dlg = findAll(host.shadow, (n) => n.attrs.role === 'dialog')[0];
assert.equal(dlg.attrs['aria-label'], 'Cookie-Einstellungen');
assert.equal(dlg.attrs['aria-label'], BANNER_ARIA);
const all = findAll(host.shadow, () => true);
const text = all.map((n) => n.textContent).join(' ');
assert.match(text, /Wir möchten mit deiner Zustimmung anonym auswerten/);
assert.ok(all.find((n) => n.tagName === 'P').textContent.startsWith(BANNER_TEXT), 'Text beginnt mit BANNER_TEXT');
const a = all.find((n) => n.tagName === 'A');
assert.equal(a.attrs.href, '/datenschutz-analyse.html');
assert.equal(a.textContent, 'Datenschutz');
@@ -244,7 +267,6 @@ test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)'
assert.match(css, /button\{[^}]*background:#0a5a8a[^}]*color:#fff/);
assert.match(css, /button\{[^}]*[; ]width:150px/);
assert.equal((css.match(/(?<![-\w])width:/g) || []).length, 2, 'eine feste Breite fuer beide + 100% mobil');
assert.ok(!/min-width/.test(css));
const portal = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'funktionen.css'), 'utf8');
assert.match(portal, /\.an-bn-btn \{[^}]*[; ]width: 150px/);
assert.equal((portal.match(/\.an-bn-btn[^{]*\{/g) || []).length, 2, 'Basisregel + mobile Regel');
@@ -258,8 +280,8 @@ test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort',
const h = harness();
const bh = hostEl(h);
btn(bh.shadow, 'Annehmen').click();
assert.equal(h.types().at(-1), 'bn:accept');
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', t: 1_700_000_000_000 });
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
assert.ok(bh.removed && !hostEl(h));
const t = target();
h.ck(t);
@@ -281,7 +303,7 @@ test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort',
assert.equal(en[0].d.scroll, 55); // (300+800)/2000
assert.equal(typeof en[0].d.sec, 'number');
// Widerruf ueber Einstellungs-Link
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 === SETTINGS_LABEL)[0];
assert.ok(settings, 'fester Link');
settings.click();
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
@@ -296,7 +318,7 @@ test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort',
});
test('w.js: gespeicherte Einwilligung => kein Banner, kein bn, Tracking an', () => {
const h = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 } });
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
assert.deepEqual(h.types(), ['pv']);
assert.equal(hostEl(h), undefined);
h.ck(target());
@@ -304,28 +326,132 @@ test('w.js: gespeicherte Einwilligung => kein Banner, kein bn, Tracking an', ()
});
test('w.js: abgelaufene/abgelehnte Einwilligung', () => {
const h = harness({ stored: { v: 1, a: 'reject', t: 1_700_000_000_000 - 5000 } });
assert.deepEqual(h.types(), ['pv']);
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 - 5000 } });
assert.deepEqual(h.types(), []);
h.ck(target()); h.leave();
assert.deepEqual(h.types(), ['pv']);
const h2 = harness({ stored: { v: 1, a: 'accept', t: 1_700_000_000_000 - 200 * 86400000 } });
assert.deepEqual(h.types(), []);
const h2 = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 200 * 86400000 } });
assert.deepEqual(h2.types(), ['pv', 'bn:shown']);
});
test('w.js: GPC und DNT => nur pv, kein Banner, nie Tracking', () => {
for (const o of [{ gpc: true }, { dnt: '1' }, { gpc: true, stored: { v: 1, a: 'accept', t: 1_700_000_000_000 } }]) {
test('w.js: GPC und DNT => nichts senden, kein Banner, keine Hooks (T1)', () => {
for (const o of [{ gpc: true }, { dnt: '1' }, { dnt: 'yes' }, { gpc: true, stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 } }]) {
const h = harness(o);
assert.deepEqual(h.types(), ['pv']);
assert.equal(hostEl(h), undefined);
assert.equal(findAll(h.body, (x) => x.textContent === 'Cookie-Einstellungen').length, 0);
h.ck(target()); h.leave();
assert.deepEqual(h.types(), ['pv']);
assert.equal(h.sent.length, 0);
assert.equal(h.winL.pageshow, undefined);
assert.equal(h.winL.popstate, undefined);
assert.equal(h.history.pushState, h.origPush);
assert.equal(h.body.children.length, 0, 'kein Banner, kein Einstellungsknopf');
h.ck(target()); h.leave(); h.nav('/neu');
assert.equal(h.sent.length, 0);
assert.equal(h.reads.referrer + h.reads.innerWidth, 0);
}
});
test('w.js: gespeichertes Ablehnen => nichts senden, nur Einstellungsknopf; spaeteres Annehmen => bn:accept, pv mit ref+vp', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
assert.equal(h.sent.length, 0);
assert.equal(h.winL.pageshow, undefined);
assert.equal(h.winL.popstate, undefined);
assert.equal(h.history.pushState, h.origPush);
assert.equal(h.body.children.length, 1);
const open = findAll(h.body, (x) => x.tagName === 'BUTTON' && x.textContent === SETTINGS_LABEL)[0];
assert.ok(open);
open.click();
assert.equal(h.sent.length, 0);
btn(hostEl(h).shadow, 'Annehmen').click();
assert.deepEqual(h.types(), ['bn:accept', 'pv']);
assert.deepEqual(h.sent[1].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
assert.ok(h.winL.pageshow && h.winL.popstate, 'Hooks nachtraeglich installiert');
h.nav('/zwei');
assert.equal(h.types().at(-1), 'pv');
assert.equal(h.sent.at(-1).d.path, '/zwei');
});
test('w.js: Ablehnen => genau ein bn:reject, danach nichts mehr (auch nicht Seitenwechsel)', () => {
const h2 = harness();
btn(hostEl(h2).shadow, 'Ablehnen').click();
assert.deepEqual(h2.types(), ['pv', 'bn:shown', 'bn:reject']);
const n = h2.sent.length;
h2.nav('/zwei'); h2.nav('/drei');
for (const f of h2.winL.pageshow || []) f({ persisted: true });
for (const f of h2.winL.popstate || []) f({});
h2.timers.splice(0).forEach((f) => f());
h2.ck(target()); h2.leave();
for (const f of h2.docL.scroll || []) f({});
assert.equal(h2.sent.length, n);
assert.equal(h2.types().filter((t) => t === 'bn:reject').length, 1);
});
test('w.js: Seitenwechsel-Timer vor dem Ablehnen sendet nichts', () => {
const h = harness();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); // Timer geplant
assert.equal(h.timers.length, 1);
btn(hostEl(h).shadow, 'Ablehnen').click();
const n = h.sent.length;
h.timers.splice(0).forEach((f) => f());
assert.equal(h.sent.length, n);
});
test('w.js: Ablehnen in anderem Tab (storage) => danach kein pv beim Seitenwechsel', () => {
const h = harness();
const n = h.sent.length;
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
h.storage();
h.nav('/zwei');
for (const f of h.winL.pageshow || []) f({ persisted: true });
assert.equal(h.sent.length, n);
});
test('w.js: Annehmen auf laufender Seite => bn:accept, genau ein cx mit ref+vp; kein zweites cx', () => {
const h = harness();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
btn(hostEl(h).shadow, 'Annehmen').click();
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
assert.deepEqual(h.sent.at(-1).d, { t: 'cx', site: SITE, path: '/start', ref: refHost('https://www.google.com/', 'mein.de'), vp: 'd' });
// Widerruf, erneut annehmen
findAll(h.body, (x) => x.textContent === SETTINGS_LABEL)[0].click();
btn(hostEl(h).shadow, 'Ablehnen').click();
findAll(h.body, (x) => x.textContent === SETTINGS_LABEL)[0].click();
btn(hostEl(h).shadow, 'Annehmen').click();
assert.equal(h.sent.filter((s) => s.d.t === 'cx').length, 1);
});
test('w.js: gespeichertes Annehmen => erstes pv mit ref+vp, kein cx', () => {
const h = harness({ stored: ACC0 });
assert.deepEqual(h.types(), ['pv']);
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
});
test('w.js: T15 kein sendBeacon, fetch mit keepalive, omit, POST, text/plain', () => {
const h = harness();
assert.ok(h.fetches.length >= 1);
assert.equal(h.beacon.length, 0);
for (const f of h.fetches) {
assert.equal(f.o.keepalive, true);
assert.equal(f.o.credentials, 'omit');
assert.equal(f.o.method, 'POST');
assert.equal(f.o.headers['Content-Type'], 'text/plain');
}
});
test('w.js: T19 Platz unten solange Banner offen; Knopf mindestens 24px', () => {
const h = harness({ bannerHeight: 90, bodyPad: '5px' });
assert.equal(h.body.style.paddingBottom, '90px');
btn(hostEl(h).shadow, 'Ablehnen').click();
assert.equal(h.body.style.paddingBottom, '5px');
const g = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
const css = findAll(g.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
assert.match(css, /min-width:24px/);
assert.match(css, /min-height:32px/);
assert.match(css, /padding:8px 12px/);
assert.match(css, /font-size:13px/);
});
test('w.js: [data-webklar-consent] oeffnet Banner, kein fester Link', () => {
const h = harness({ stored: { v: 1, a: 'reject', t: 1_700_000_000_000 }, withConsentLink: true });
assert.equal(findAll(h.body, (x) => x.textContent === 'Cookie-Einstellungen').length, 0);
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 }, withConsentLink: true });
assert.equal(findAll(h.body, (x) => x.textContent === SETTINGS_LABEL).length, 0);
assert.equal(hostEl(h), undefined);
h.link.click();
assert.ok(hostEl(h));
@@ -343,14 +469,14 @@ test('w.js: Heatmap-Modus sendet nichts, entfernt Parameter, laedt h.js', () =>
});
test('w.js: SPA-Navigation sendet pv nur bei Pfadwechsel', () => {
const h = harness({ gpc: true });
h.win.location.pathname = '/start';
h.history.pushState(null, '', '/start?x=1'); h.timers.splice(0).forEach((f) => f());
assert.equal(h.sent.length, 1);
const h = harness();
h.nav('/start');
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
h.win.location.pathname = '/neu';
h.history.pushState(null, '', '/neu'); h.history.pushState(null, '', '/neu');
h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.map((s) => s.d.path), ['/start', '/neu']);
assert.deepEqual(h.sent.filter((s) => s.d.t === 'pv').map((s) => s.d.path), ['/start', '/neu']);
assert.deepEqual(h.sent.at(-1).d, { t: 'pv', site: SITE, path: '/neu' });
});
test('w.js: wirft nie, auch wenn localStorage/sendBeacon kaputt', () => {
@@ -361,36 +487,33 @@ test('w.js: wirft nie, auch wenn localStorage/sendBeacon kaputt', () => {
});
});
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 });
const h = harness();
h.rerun();
assert.deepEqual(h.types(), ['pv']);
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
});
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 } });
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, 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.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, 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.store['wk-consent'] = JSON.stringify({ v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
h.storage();
h.ck(target());
assert.equal(h.sent.length, n + 1);
assert.equal(h.sent.length, n + 1); // nur ck, kein cx (pv trug ref/vp schon)
assert.ok(!h.sent.some((x) => x.d.t === 'cx'));
});
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 h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
const n = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, n);
@@ -398,13 +521,13 @@ test('w.js: Einwilligung wird vor jedem Senden neu geprueft (ohne storage-Ereign
});
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 } });
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, 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.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
h.storage();
const n = h.sent.length;
h.leave();
@@ -412,7 +535,7 @@ test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () =>
});
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 } });
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, 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 });
@@ -424,7 +547,7 @@ test('w.js: bfcache-Rueckkehr sendet neues pv und erlaubt neues en', () => {
});
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 } });
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, 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() {} });
@@ -433,7 +556,7 @@ test('w.js: [data-webklar-consent] spaeter hinzugefuegt funktioniert per Delegat
});
// ---------- Verweildauer bei internem Seitenwechsel ----------
const ACC = { v: 1, a: 'accept', t: 1_700_000_000_000 - 1000 };
const ACC = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
const lnk = (href, attrs = {}) => { const a = makeEl('a'); a.setAttribute('href', href); for (const k in attrs) a.setAttribute(k, attrs[k]); a.parentNode = makeEl('body'); return a; };
const enCount = (h) => h.sent.filter((s) => s.d.t === 'en').length;
@@ -473,7 +596,7 @@ test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
h.ck(lnk('/kontakt'));
assert.equal(enCount(h), 0);
const g = harness({ stored: ACC });
g.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', t: 1_700_000_000_000 });
g.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
g.ck(lnk('/kontakt'));
assert.equal(enCount(g), 0);
});