Router-Links (z. B. React Router auf webklar.com) verhindern den Standard-Klick, dann griff onLinkClick nicht und das en der vorigen Seite ging verloren. Jetzt sendet der Seitenwechsel-Timer zuerst en (nur mit Einwilligung, einmal je Seite) und dann das pv der neuen Seite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
710 lines
36 KiB
JavaScript
710 lines
36 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import vm from 'node:vm';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
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, BANNER_VERSION_EIGEN, BANNER_TEXT_EIGEN, bannerFor } 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 ----------
|
|
test('vpClass', () => {
|
|
assert.equal(vpClass(0), 'm');
|
|
assert.equal(vpClass(767), 'm');
|
|
assert.equal(vpClass(768), 't');
|
|
assert.equal(vpClass(1023), 't');
|
|
assert.equal(vpClass(1024), 'd');
|
|
assert.equal(vpClass(NaN), 'd');
|
|
});
|
|
|
|
test('cleanPath', () => {
|
|
assert.equal(cleanPath('https://x.de/a/b?q=1#h'), '/a/b');
|
|
assert.equal(cleanPath('/über%20uns?x=1'), '/über uns');
|
|
assert.equal(cleanPath('/%E0%A4%A'), '/%E0%A4%A'); // kaputtes Encoding bleibt roh
|
|
assert.equal(cleanPath('https://x.de'), '/');
|
|
assert.equal(cleanPath(''), '/');
|
|
assert.equal(cleanPath('/' + 'a'.repeat(400)).length, 300);
|
|
assert.equal(cleanPath(null), '/');
|
|
assert.equal(cleanPath('//foo/bar?x=1'), '//foo/bar');
|
|
assert.equal(cleanPath('//foo'), '//foo');
|
|
});
|
|
|
|
test('refHost', () => {
|
|
assert.equal(refHost('https://www.google.com/search?q=x', 'mein.de'), 'www.google.com');
|
|
assert.equal(refHost('https://mein.de/a', 'mein.de'), '');
|
|
assert.equal(refHost('https://www.mein.de/a', 'mein.de'), '');
|
|
assert.equal(refHost('', 'mein.de'), '');
|
|
assert.equal(refHost('kein url', 'mein.de'), '');
|
|
assert.equal(refHost('android-app://com.x', 'mein.de'), '');
|
|
assert.equal(refHost('HTTPS://Foo.DE/x', 'mein.de'), 'foo.de');
|
|
});
|
|
|
|
function el(tag, o = {}) {
|
|
const e = { tagName: tag.toUpperCase(), id: o.id || '', parentNode: o.parent || null, previousElementSibling: o.prev || null,
|
|
attrs: { class: o.cls || '' }, getAttribute(n) { return this.attrs[n] ?? null; } };
|
|
return e;
|
|
}
|
|
|
|
test('cssPath: nur Tags + nth-of-type, keine IDs/Klassen', () => {
|
|
const body = el('body');
|
|
const div = el('div', { parent: body, cls: 'hero main' });
|
|
const p1 = el('p', { parent: div });
|
|
const btn0 = el('button', { parent: div, prev: p1 });
|
|
const btn = el('button', { parent: div, prev: btn0, cls: 'cta' });
|
|
assert.equal(cssPath(btn), 'body:nth-of-type(1) > div:nth-of-type(1) > button:nth-of-type(2)');
|
|
const pii = el('a', { parent: body, id: 'max@firma.de', cls: 'team-max-mustermann' });
|
|
const sel = cssPath(pii);
|
|
assert.equal(sel, 'body:nth-of-type(1) > a:nth-of-type(1)');
|
|
assert.ok(!/max|firma|mustermann|#|\./.test(sel));
|
|
// max. 6 Ebenen
|
|
let n = el('div');
|
|
for (let i = 0; i < 9; i++) n = el('span', { parent: n });
|
|
assert.equal(cssPath(n).split(' > ').length, 6);
|
|
// stoppt bei body
|
|
const outer = el('html'); body.parentNode = outer;
|
|
assert.equal(cssPath(div).split(' > ')[0], 'body:nth-of-type(1)');
|
|
// <= 200
|
|
let deep = el('div');
|
|
for (let i = 0; i < 5; i++) deep = el('x'.repeat(40), { parent: deep });
|
|
assert.ok(cssPath(deep).length <= 200);
|
|
assert.equal(cssPath(null), '');
|
|
});
|
|
|
|
test('contactKind', () => {
|
|
const k = (href) => contactKind({ href }, 'mein.de');
|
|
assert.equal(k('tel:+49123'), 'tel');
|
|
assert.equal(k('TEL:+49123'), 'tel');
|
|
assert.equal(k('mailto:a@b.de'), 'mail');
|
|
assert.equal(k('https://wa.me/49123'), 'wa');
|
|
assert.equal(k('https://api.whatsapp.com/send?phone=1'), 'wa');
|
|
assert.equal(k('whatsapp://send?text=x'), 'wa');
|
|
assert.equal(k('https://anders.de/x'), 'out');
|
|
assert.equal(k('https://mein.de/x'), null);
|
|
assert.equal(k('https://www.mein.de/x'), null);
|
|
assert.equal(k('/kontakt'), null);
|
|
assert.equal(k('#top'), null);
|
|
assert.equal(k('javascript:void(0)'), null);
|
|
assert.equal(k(''), null);
|
|
assert.equal(contactKind(null, 'mein.de'), null);
|
|
});
|
|
|
|
test('readConsent', () => {
|
|
const now = 1_700_000_000_000;
|
|
const day = 86400000;
|
|
const st = (v) => ({ getItem: () => v });
|
|
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);
|
|
assert.equal(readConsent(null, now), null);
|
|
});
|
|
|
|
test('contrastOk', () => {
|
|
assert.equal(contrastOk('#111111'), true);
|
|
assert.equal(contrastOk('#ffffff'), false);
|
|
assert.equal(contrastOk('#ffff00'), false);
|
|
assert.equal(contrastOk('#767676'), true); // 4.54
|
|
assert.equal(contrastOk('#777777'), false); // 4.48
|
|
assert.equal(contrastOk('#00F'), false); // ungueltig
|
|
assert.equal(contrastOk(undefined), false);
|
|
});
|
|
|
|
test('escapeHtml', () => {
|
|
assert.equal(escapeHtml(`<a href="x">&'</a>`), '<a href="x">&'</a>');
|
|
assert.equal(escapeHtml(null), '');
|
|
});
|
|
|
|
// ---------- dist ----------
|
|
test('dist ist aktuell', () => {
|
|
const b = buildAll();
|
|
assert.equal(readFileSync(join(DIST, 'w.js'), 'utf8'), b.w);
|
|
assert.equal(readFileSync(join(DIST, 'h.js'), 'utf8'), b.h);
|
|
assert.ok(!/^\s*(import|export)\s/m.test(b.w + b.h));
|
|
});
|
|
|
|
// ---------- Verhalten von w.js (Fake-DOM) ----------
|
|
function makeEl(tag) {
|
|
const e = {
|
|
tagName: String(tag).toUpperCase(), attrs: {}, children: [], listeners: {}, style: {}, parentNode: null, id: '',
|
|
textContent: '', focused: false, removed: false,
|
|
setAttribute(n, v) { this.attrs[n] = String(v); if (n === 'id') this.id = String(v); },
|
|
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; },
|
|
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
append(...cs) { cs.forEach((c) => this.appendChild(c)); },
|
|
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);
|
|
for (let n = this; n; n = n.parentNode) if (attr && n.attrs && attr[1] in n.attrs) return n;
|
|
return null;
|
|
},
|
|
attachShadow() { const r = makeEl('shadow'); r.isShadow = true; this.shadow = r; return r; },
|
|
click(extra = {}) { fire(this, 'click', { target: this, ...extra }); },
|
|
};
|
|
Object.defineProperty(e, 'href', { get() { return this.attrs.href || ''; } });
|
|
Object.defineProperty(e, 'className', { set(v) { this.attrs.class = v; }, get() { return this.attrs.class || ''; } });
|
|
return e;
|
|
}
|
|
function fire(node, type, ev) { for (const f of [...(node.listeners[type] || [])]) f({ type, preventDefault() {}, ...ev }); }
|
|
function walk(n, fn) { fn(n); for (const c of n.children || []) walk(c, fn); if (n.shadow) walk(n.shadow, fn); }
|
|
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];
|
|
const settingsBtn = (h) => findAll(h.body, (n) => n.tagName === 'BUTTON' && n.attrs['aria-label'] === SETTINGS_LABEL)[0];
|
|
|
|
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad, variant } = {}) {
|
|
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'); 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'); if (variant !== undefined) script.setAttribute('data-variant', variant); script.src = src;
|
|
const docL = {}; const winL = {};
|
|
const link = withConsentLink ? makeEl('a') : null; if (link) link.setAttribute('data-webklar-consent', '');
|
|
const injected = [];
|
|
if (link) link.click = () => { for (const f of [...(docL.click || [])]) f({ type: 'click', target: link, preventDefault() {} }); };
|
|
const document = {
|
|
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(t) { const e = makeEl(t); if (bannerHeight) e.offsetHeight = bannerHeight; return e; }, activeElement: null,
|
|
createElementNS(ns, t) { const e = makeEl(t); e.ns = ns; return e; },
|
|
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 origPush = function () {};
|
|
const history = { pushState: origPush, replaceState(s, t, u) { this.replaced = u; } };
|
|
const win = {
|
|
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) { 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: (u, o) => { fetches.push({ u, o }); sent.push({ u, d: JSON.parse(o.body) }); return Promise.resolve(); },
|
|
};
|
|
win.localStorage = sandbox.localStorage;
|
|
sandbox.globalThis = sandbox;
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
|
|
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 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' }));
|
|
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);
|
|
|
|
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: 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'], BANNER_ARIA);
|
|
const all = findAll(host.shadow, () => true);
|
|
const text = all.map((n) => n.textContent).join(' ');
|
|
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');
|
|
const css = all.filter((n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
|
|
assert.match(css, /#0a5a8a/);
|
|
assert.match(css, /max-width:\s*767px/);
|
|
const buttons = all.filter((n) => n.tagName === 'BUTTON');
|
|
assert.deepEqual(buttons.map((b) => b.textContent), ['Ablehnen', 'Annehmen']);
|
|
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.match(css, /button\{[^}]*[; ]width:150px/);
|
|
assert.equal((css.match(/(?<![-\w])width:/g) || []).length, 2, 'eine feste Breite fuer beide + 100% mobil');
|
|
const portal = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'analyse.css'), 'utf8');
|
|
assert.match(portal, /\.an-bn-btn \{[^}]*[; ]width: 150px/);
|
|
assert.equal((portal.match(/\.an-bn-btn[^{]*\{/g) || []).length, 2, 'Basisregel + mobile Regel');
|
|
assert.equal(host.style.zIndex, '2147483647');
|
|
// Escape schliesst nicht
|
|
fire(dlg, 'keydown', { key: 'Escape' }); fire(host, 'keydown', { key: 'Escape' });
|
|
assert.ok(!host.removed);
|
|
});
|
|
|
|
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.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);
|
|
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:nth-of-type(1) > button:nth-of-type(1)' });
|
|
// Kontakt-Link: ck + ct (Kind-Element in <a>)
|
|
const a = makeEl('a'); a.setAttribute('href', 'tel:+4912'); a.parentNode = makeEl('body');
|
|
const span = makeEl('span'); span.parentNode = a;
|
|
const n = h.sent.length; h.ck(span);
|
|
assert.deepEqual(h.sent.slice(n).map((s) => s.d.t), ['ck', 'ct']);
|
|
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'tel' });
|
|
const out = makeEl('a'); out.setAttribute('href', 'https://anders.de/x'); out.parentNode = makeEl('body');
|
|
h.ck(out);
|
|
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'out', host: 'anders.de' });
|
|
// en einmal
|
|
h.leave(); h.leave();
|
|
const en = h.sent.filter((s) => s.d.t === 'en');
|
|
assert.equal(en.length, 1);
|
|
assert.equal(en[0].d.scroll, 55); // (300+800)/2000
|
|
assert.equal(typeof en[0].d.sec, 'number');
|
|
// Widerruf ueber Einstellungs-Link
|
|
const settings = settingsBtn(h);
|
|
assert.ok(settings, 'fester Link');
|
|
settings.click();
|
|
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
|
|
btn(hostEl(h).shadow, 'Ablehnen').click();
|
|
assert.equal(h.types().at(-1), 'bn:reject');
|
|
const before = h.sent.length;
|
|
h.ck(target()); h.leave();
|
|
assert.equal(h.sent.length, before);
|
|
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
|
|
assert.equal((h.docL.visibilitychange || []).length, 0);
|
|
assert.equal((h.winL.pagehide || []).length, 0);
|
|
});
|
|
|
|
test('w.js: gespeicherte Einwilligung => kein Banner, kein bn, Tracking an', () => {
|
|
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());
|
|
assert.equal(h.sent.at(-1).d.t, 'ck');
|
|
});
|
|
|
|
test('w.js: abgelaufene/abgelehnte Einwilligung', () => {
|
|
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(), []);
|
|
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 => 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.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 = settingsBtn(h);
|
|
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
|
|
settingsBtn(h).click();
|
|
btn(hostEl(h).shadow, 'Ablehnen').click();
|
|
settingsBtn(h).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, /width:44px;height:44px/);
|
|
});
|
|
|
|
test('w.js: Einstellungsknopf ist ein angeknabberter Keks, der sich dreht (ohne Drehung bei reduzierter Bewegung)', () => {
|
|
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
|
|
const b = settingsBtn(h);
|
|
assert.ok(b, 'Knopf da');
|
|
assert.equal(b.attrs.type, 'button');
|
|
assert.equal(b.textContent, '', 'kein sichtbarer Text im Knopf selbst');
|
|
// Etikett "Datenschutz-Einstellungen" erscheint beim Darueberfahren, fuer Screenreader zaehlt aria-label
|
|
const label = findAll(b, (n) => n.textContent === SETTINGS_LABEL)[0];
|
|
assert.ok(label);
|
|
assert.equal(label.attrs['aria-hidden'], 'true');
|
|
// Keks als Inline-SVG (keine data:-Grafik, die eine CSP der Website blockieren koennte)
|
|
const svg = findAll(b, (n) => n.tagName === 'SVG')[0];
|
|
assert.ok(svg);
|
|
assert.equal(svg.ns, 'http://www.w3.org/2000/svg');
|
|
assert.equal(svg.attrs['aria-hidden'], 'true');
|
|
const paths = findAll(svg, (n) => n.tagName === 'PATH');
|
|
assert.ok(paths.length >= 1, 'Umriss mit Bissspur');
|
|
assert.match(paths[0].attrs.d, /A/, 'Bogen fuer den Biss');
|
|
assert.ok(findAll(svg, (n) => n.tagName === 'ELLIPSE' || n.tagName === 'CIRCLE').length >= 5, 'Schokostueckchen');
|
|
const css = findAll(h.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
|
|
assert.match(css, /@keyframes wk-spin\{to\{transform:rotate\(360deg\)\}\}/);
|
|
assert.match(css, /animation:wk-spin \d+s linear infinite/);
|
|
assert.match(css, /@media \(prefers-reduced-motion:reduce\)\{[^}]*animation:none/);
|
|
assert.match(css, /:hover \.c,[^{]*:focus-visible \.c\{animation-play-state:paused\}/);
|
|
assert.ok(!/url\(/.test(css), 'keine externen oder data:-Grafiken');
|
|
b.click();
|
|
assert.ok(hostEl(h), 'Klick oeffnet das Banner');
|
|
});
|
|
|
|
test('w.js: [data-webklar-consent] oeffnet Banner, kein fester Link', () => {
|
|
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 }, withConsentLink: true });
|
|
assert.equal(settingsBtn(h), undefined);
|
|
assert.equal(hostEl(h), undefined);
|
|
h.link.click();
|
|
assert.ok(hostEl(h));
|
|
});
|
|
|
|
test('w.js: Heatmap-Modus sendet nichts, entfernt Parameter, laedt h.js', () => {
|
|
const h = harness({ search: '?a=1&__wk_heat=TOKEN.sig' });
|
|
assert.deepEqual(h.sent, []);
|
|
assert.equal(hostEl(h), undefined);
|
|
assert.equal(h.history.replaced, '/start?a=1');
|
|
assert.equal(h.injected.length, 1);
|
|
assert.equal(h.injected[0].attrs.src, 'https://project.webklar.com/a/h.js');
|
|
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
|
|
assert.equal(h.injected[0].attrs['data-embed'], undefined);
|
|
assert.equal((h.docL.click || []).length, 0);
|
|
});
|
|
|
|
test('w.js: Heatmap-Modus mit __wk_embed=1 entfernt beide Parameter und reicht data-embed an h.js', () => {
|
|
const h = harness({ search: '?__wk_heat=TOKEN.sig&__wk_embed=1&b=2' });
|
|
assert.deepEqual(h.sent, []);
|
|
assert.equal(h.history.replaced, '/start?b=2');
|
|
assert.equal(h.injected.length, 1);
|
|
assert.equal(h.injected[0].attrs['data-embed'], '1');
|
|
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
|
|
// __wk_embed ohne Heatmap-Token: normaler Betrieb (Banner, pv)
|
|
const n = harness({ search: '?__wk_embed=1' });
|
|
assert.deepEqual(n.types(), ['pv', 'bn:shown']);
|
|
});
|
|
|
|
test('w.js: SPA-Navigation sendet pv nur bei Pfadwechsel', () => {
|
|
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.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', () => {
|
|
assert.doesNotThrow(() => {
|
|
const sandbox = { window: {}, document: { currentScript: null, querySelector() { throw new Error('x'); } }, navigator: {}, location: {} };
|
|
sandbox.window = sandbox; vm.createContext(sandbox);
|
|
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
|
|
});
|
|
});
|
|
|
|
test('w.js: doppelt eingebunden => nur ein pv', () => {
|
|
const h = harness();
|
|
h.rerun();
|
|
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', 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', 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', bv: BV, t: 1_700_000_000_000 });
|
|
h.storage();
|
|
h.ck(target());
|
|
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', 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);
|
|
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', 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());
|
|
// en der vorigen Seite vor dem pv der neuen (Router-Links verhindern den Standard-Klick, onLinkClick greift nicht)
|
|
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
|
assert.equal(h.sent[1].d.scroll, 55);
|
|
h.ck(target());
|
|
assert.equal(h.sent.at(-1).d.path, '/zwei');
|
|
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();
|
|
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', 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 });
|
|
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', 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() {} });
|
|
assert.ok(hostEl(h));
|
|
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused);
|
|
});
|
|
|
|
// ---------- Verweildauer bei internem Seitenwechsel ----------
|
|
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;
|
|
|
|
test('w.js: interner Link-Klick sendet en sofort, pagehide danach nicht doppelt', () => {
|
|
const h = harness({ stored: ACC });
|
|
h.ck(lnk('/kontakt'));
|
|
assert.equal(enCount(h), 1);
|
|
assert.deepEqual(h.sent.map((s) => s.d.t), ['pv', 'ck', 'en']);
|
|
const en = h.sent.at(-1).d;
|
|
assert.equal(en.path, '/start'); assert.equal(en.scroll, 55);
|
|
h.leave();
|
|
assert.equal(enCount(h), 1);
|
|
h.ck(lnk('https://mein.de/andere?x=1#a'));
|
|
assert.equal(enCount(h), 1);
|
|
});
|
|
|
|
test('w.js: kein fruehes en bei externem Link, Anker, gleicher Seite, Modifier, Ziel/Download, defaultPrevented', () => {
|
|
const h = harness({ stored: ACC });
|
|
h.ck(lnk('https://anders.de/x'));
|
|
h.ck(lnk('#oben'));
|
|
h.ck(lnk('/start#unten'));
|
|
h.ck(lnk('/kontakt'), { ctrlKey: true });
|
|
h.ck(lnk('/kontakt'), { metaKey: true });
|
|
h.ck(lnk('/kontakt'), { shiftKey: true });
|
|
h.ck(lnk('/kontakt'), { button: 1 });
|
|
h.ck(lnk('/kontakt', { target: '_blank' }));
|
|
h.ck(lnk('/datei.pdf', { download: '' }));
|
|
h.ck(lnk('/kontakt'), { defaultPrevented: true });
|
|
h.ck(lnk('mailto:a@b.de'));
|
|
assert.equal(enCount(h), 0);
|
|
h.leave();
|
|
assert.equal(enCount(h), 1);
|
|
});
|
|
|
|
test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
|
|
const h = harness();
|
|
h.ck(lnk('/kontakt'));
|
|
assert.equal(enCount(h), 0);
|
|
const g = harness({ stored: ACC });
|
|
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);
|
|
});
|
|
|
|
// ---------- Variante "eigen" (WEBklars eigene Websites, z. B. webklar.com) ----------
|
|
test('core: bannerFor und readConsent je Fassung', () => {
|
|
assert.deepEqual(bannerFor('eigen'), { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN });
|
|
for (const v of ['', 'kunde', 'EIGEN', 'x', undefined]) assert.deepEqual(bannerFor(v), { version: BANNER_VERSION, text: BANNER_TEXT });
|
|
assert.equal(BANNER_VERSION_EIGEN, '2026-10-17');
|
|
assert.notEqual(BANNER_VERSION_EIGEN, BANNER_VERSION);
|
|
const now = 1_700_000_000_000;
|
|
const st = (o) => ({ getItem: () => JSON.stringify(o) });
|
|
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BV, t: now }), now, BANNER_VERSION_EIGEN), null, 'Kunden-Fassung gilt nicht fuer eigen');
|
|
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now, BANNER_VERSION_EIGEN).a, 'accept');
|
|
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now), null, 'ohne Fassung gilt die Kunden-Fassung');
|
|
});
|
|
|
|
test('core: Banner-Text eigen (Sie-Form, ohne Auftragsverarbeiter)', () => {
|
|
assert.match(BANNER_TEXT_EIGEN, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
|
|
assert.match(BANNER_TEXT_EIGEN, /Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung\./);
|
|
assert.match(BANNER_TEXT_EIGEN, /Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben\./);
|
|
assert.match(BANNER_TEXT_EIGEN, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
|
|
assert.doesNotMatch(BANNER_TEXT_EIGEN, /\b(Du|Dein|Deine|Deiner|Dich|Dir)\b/);
|
|
assert.doesNotMatch(BANNER_TEXT_EIGEN, /betreibt für uns/);
|
|
});
|
|
|
|
test('w.js: data-variant="eigen" => Sie-Text, eigene Fassung im Speicher', () => {
|
|
const h = harness({ variant: 'eigen' });
|
|
const host = hostEl(h);
|
|
const p = findAll(host.shadow, (n) => n.tagName === 'P')[0];
|
|
assert.ok(p.textContent.startsWith(BANNER_TEXT_EIGEN));
|
|
assert.ok(!p.textContent.startsWith(BANNER_TEXT));
|
|
btn(host.shadow, 'Annehmen').click();
|
|
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 });
|
|
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
|
|
});
|
|
|
|
test('w.js: eigen fragt neu bei Wahl aus der Kunden-Fassung, gilt mit eigener Fassung', () => {
|
|
const alt = harness({ variant: 'eigen', stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
|
assert.ok(hostEl(alt), 'Banner erscheint erneut');
|
|
const ok = harness({ variant: 'eigen', stored: { v: 1, a: 'reject', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 - 1000 } });
|
|
assert.equal(hostEl(ok), undefined);
|
|
assert.deepEqual(ok.types(), []);
|
|
});
|
|
|
|
test('w.js: unbekannte Variante => Kunden-Fassung', () => {
|
|
const h = harness({ variant: 'sonstwas' });
|
|
const p = findAll(hostEl(h).shadow, (n) => n.tagName === 'P')[0];
|
|
assert.ok(p.textContent.startsWith(BANNER_TEXT));
|
|
});
|
|
|
|
test('w.js: SPA-Wechsel ohne Einwilligung sendet nur pv, kein en', () => {
|
|
const h = harness();
|
|
h.win.location.pathname = '/zwei';
|
|
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
|
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'pv']);
|
|
});
|
|
|
|
test('w.js: en nach SPA-Wechsel nur einmal je Seite (Link-Klick und Router)', () => {
|
|
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
|
h.leave();
|
|
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.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
|
h.leave();
|
|
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`).slice(-1), ['en:/zwei']);
|
|
});
|