""" Browser-side DOM walkers for Web-Component CMPs and OEM design-systems. Centralizes the JavaScript snippets used by banner_text_checker.py so the checker file stays under the 500-LOC cap. Each function returns a JS string that Playwright passes to `page.evaluate()`. Two walkers: * SHADOW_BANNER_WALKER_JS — pierces shadow DOM (Mercedes cmm-cookie-banner, BMW cookie-consent-banner, etc.) and extracts banner text + label-based legal links (P63 — recognizes wb7-link/role=link/button, not just , since OEM design-systems wrap navigation). * FOOTER_LABELS_WALKER_JS — collects unique footer link labels from any candidate footer root (footer, [role=contentinfo], wb7-footer, ...) with a bottom-25%-of-viewport fallback (P64). """ from __future__ import annotations SHADOW_BANNER_WALKER_JS = """() => { const LEGAL_KW = { impressum: ['impressum','imprint','legal notice','mentions legales','colophon'], dse: ['datenschutz','privacy','dsgvo','data protection','politique de confidentialite'], }; function isLegalLabel(txt) { const t = (txt||'').toLowerCase(); if (!t || t.length > 60) return null; for (const k of LEGAL_KW.impressum) if (t.includes(k)) return 'impressum'; for (const k of LEGAL_KW.dse) if (t.includes(k)) return 'dse'; return null; } function walk(root, acc) { if (!root) return; const all = root.querySelectorAll ? root.querySelectorAll('*') : []; for (const el of all) { if (el.shadowRoot) walk(el.shadowRoot, acc); } const tags = ['cmm-cookie-banner', 'cookie-consent-banner', 'consent-banner', 'cookie-banner', 'cmp-banner', 'ot-banner', 'usercentrics-banner']; for (const tag of tags) { const els = root.querySelectorAll ? root.querySelectorAll(tag) : []; for (const el of els) { if (el.shadowRoot) { const txt = (el.shadowRoot.textContent || '').trim(); if (txt) acc.text += ' ' + txt; const links = el.shadowRoot.querySelectorAll('a[href]'); for (const a of links) { acc.links.push({ href: (a.getAttribute('href') || '').toLowerCase(), text: (a.textContent || '').trim().toLowerCase(), }); } const cands = el.shadowRoot.querySelectorAll( 'wb7-link, wb7-button, [role="link"], button, span, a' ); for (const c of cands) { const label = (c.textContent || '').trim(); const which = isLegalLabel(label); if (which) { const href = (c.getAttribute('href') || c.getAttribute('data-href') || c.getAttribute('data-uri') || '').toLowerCase(); acc.links.push({ href: href || ('#shadow-' + which), text: label.toLowerCase(), }); } } } } } } const acc = { text: '', links: [] }; walk(document, acc); return acc; }""" FOOTER_LABELS_WALKER_JS = """() => { const out = new Set(); const roots = [ ...document.querySelectorAll( 'footer, [role="contentinfo"], ' + 'wb7-footer, wb-footer, b-footer, cmm-footer, ' + '[class*="footer" i], [id*="footer" i]' ) ]; if (roots.length === 0) { const viewH = window.innerHeight; for (const el of document.querySelectorAll('a, button, [role="link"], wb7-link')) { const r = el.getBoundingClientRect(); if (r.top > viewH * 0.75) roots.push(el.parentElement); } } for (const root of roots) { if (!root) continue; const cands = root.querySelectorAll('a, button, [role="link"], wb7-link, wb7-button'); let n = 0; for (const c of cands) { if (n++ > 80) break; const t = (c.textContent || '').trim().toLowerCase(); if (t && t.length < 60) out.add(t); } } return [...out]; }"""