Heatmap in Geraeterahmen (Computer/Tablet + Handy) und Keks statt "Datenschutz-Einstellungen"
Heatmap im Portal (Funktionen -> Analyse): die echte Website laeuft jetzt in realistischen Geraeterahmen, gross MacBook oder iPad (Umschalter oben), daneben das iPhone, jeweils in echter Breite (1512 / 820 / 402 px), verkleinert und im Geraet scrollbar, mit den Klicks der passenden Geraeteklasse als Overlay. MacBook und iPhone stammen aus den Rahmen der neuen Website (device-mockups), das iPad ist im gleichen Stil nachgebaut (public/geraete.css/.js). - heatmap-link: embed: true haengt __wk_embed=1 an; w.js reicht das als data-embed an h.js. - h.js eingebettet (nur in einem Rahmen): keine Leiste, Links und Formulare gesperrt (kein Seitenwechsel, also keine Zaehlung und kein Banner), Scrollleisten aus, Klickzahl per postMessage nur an die Herkunft von h.js (project.webklar.com). - iframe mit Sandbox allow-scripts allow-same-origin (keine Formulare, Pop-ups, keine Navigation des Portals); Laden erst, wenn die Heatmap in Sicht kommt. Laesst sich eine Website nicht einbetten, kommt nach 20 s ein Hinweis mit "In neuem Tab oeffnen" (alter Weg). Banner-Skript: der Einstellungsknopf nach der Wahl ist jetzt ein angeknabberter Keks unten rechts (Inline-SVG, 44 px). Er dreht sich langsam, haelt beim Darueberfahren/Fokus an und zeigt dann "Datenschutz-Einstellungen" (auch sein Name fuer Screenreader, passend zum Bannertext); bei reduzierter Bewegung dreht er sich nicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff7fe35c06
commit
0a113c2983
14 files changed
+1658
-91
No files matched your search
@@ -168,6 +168,7 @@ function fire(node, type, ev) { for (const f of [...(node.listeners[type] || [])
|
||||
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 } = {}) {
|
||||
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
|
||||
@@ -183,6 +184,7 @@ function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar
|
||||
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); },
|
||||
@@ -303,7 +305,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 === SETTINGS_LABEL)[0];
|
||||
const settings = settingsBtn(h);
|
||||
assert.ok(settings, 'fester Link');
|
||||
settings.click();
|
||||
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
|
||||
@@ -355,7 +357,7 @@ test('w.js: gespeichertes Ablehnen => nichts senden, nur Einstellungsknopf; spae
|
||||
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];
|
||||
const open = settingsBtn(h);
|
||||
assert.ok(open);
|
||||
open.click();
|
||||
assert.equal(h.sent.length, 0);
|
||||
@@ -411,9 +413,9 @@ test('w.js: Annehmen auf laufender Seite => bn:accept, genau ein cx mit ref+vp;
|
||||
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();
|
||||
settingsBtn(h).click();
|
||||
btn(hostEl(h).shadow, 'Ablehnen').click();
|
||||
findAll(h.body, (x) => x.textContent === SETTINGS_LABEL)[0].click();
|
||||
settingsBtn(h).click();
|
||||
btn(hostEl(h).shadow, 'Annehmen').click();
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'cx').length, 1);
|
||||
});
|
||||
@@ -443,15 +445,41 @@ test('w.js: T19 Platz unten solange Banner offen; Knopf mindestens 24px', () =>
|
||||
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/);
|
||||
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(findAll(h.body, (x) => x.textContent === SETTINGS_LABEL).length, 0);
|
||||
assert.equal(settingsBtn(h), undefined);
|
||||
assert.equal(hostEl(h), undefined);
|
||||
h.link.click();
|
||||
assert.ok(hostEl(h));
|
||||
@@ -465,9 +493,22 @@ test('w.js: Heatmap-Modus sendet nichts, entfernt Parameter, laedt h.js', () =>
|
||||
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');
|
||||
|
||||
Reference in new issue
Block a user