fe for production screenshot pipelines.
Step 1: Define the Rule Schema
Each CMP follows a predictable DOM structure. We map these structures into a typed rule registry. The schema separates interaction targets from suppression targets to prevent layout collapse.
interface ConsentRule {
platform: string;
interactionTargets: string[];
suppressionContainers: string[];
priority: number;
}
type RuleRegistry = ConsentRule[];
Step 2: Build the Visibility-Aware Interaction Layer
Headless browsers throw errors when attempting to click elements that are not rendered or are obscured by other layers. The engine must verify visibility before dispatching click events.
async function attemptInteraction(
page: any,
selectors: string[]
): Promise<boolean> {
for (const selector of selectors) {
const element = await page.$(selector);
if (!element) continue;
const isVisible = await page.evaluate((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 &&
window.getComputedStyle(el).display !== 'none';
}, element);
if (isVisible) {
await element.click();
return true;
}
}
return false;
}
Step 3: Implement Container Suppression
When explicit buttons are missing or dynamically loaded, hiding the parent container is safer than removing it from the DOM. Removal can trigger JavaScript errors in CMP scripts that expect their root element to persist.
async function suppressContainers(
page: any,
containerSelectors: string[]
): Promise<void> {
await page.evaluate((selectors) => {
selectors.forEach((sel) => {
const nodes = document.querySelectorAll(sel);
nodes.forEach((node) => {
if (node instanceof HTMLElement) {
node.style.setProperty('display', 'none', 'important');
node.style.setProperty('visibility', 'hidden', 'important');
}
});
});
}, containerSelectors);
}
Step 4: Add Heuristic Text Matching
Some banners generate selectors dynamically or use A/B tested markup. A regex-based fallback scans visible buttons for standard consent language.
const CONSENT_KEYWORDS = /^(accept all|accept|i agree|got it|ok|continue)$/i;
async function fallbackTextMatch(page: any): Promise<boolean> {
const matched = await page.evaluate((pattern) => {
const buttons = Array.from(document.querySelectorAll('button, a, [role="button"]'));
const target = buttons.find((btn) => {
const text = btn.textContent?.trim() || '';
const rect = btn.getBoundingClientRect();
return pattern.test(text) && rect.width > 0 && rect.height > 0;
});
if (target) {
(target as HTMLElement).click();
return true;
}
return false;
}, CONSENT_KEYWORDS);
return matched;
}
Step 5: Orchestrate the Execution Pipeline
The engine iterates through the rule registry in priority order. It attempts explicit interaction first, falls back to container suppression, and finally triggers the text heuristic. This sequence ensures maximum compatibility while minimizing DOM mutation side effects.
async function executeDismissSequence(page: any, rules: RuleRegistry): Promise<void> {
const sortedRules = [...rules].sort((a, b) => a.priority - b.priority);
for (const rule of sortedRules) {
const clicked = await attemptInteraction(page, rule.interactionTargets);
if (clicked) return;
await suppressContainers(page, rule.suppressionContainers);
}
await fallbackTextMatch(page);
}
Architecture Rationale:
- Priority Sorting: CMPs like OneTrust and Cookiebot dominate enterprise deployments. Placing them first reduces evaluation time for high-traffic targets.
- Visibility Checks: Prevents
Node is not visible exceptions in Playwright/Puppeteer and avoids clicking hidden overlay traps.
- Container Hiding vs Removal: Hiding preserves event listeners and prevents CMP scripts from throwing
getElementById errors during post-load initialization.
- Regex Fallback: Covers custom implementations, regional variants, and dynamically generated markup without requiring selector updates.
Pitfall Guide
Automated consent dismissal introduces subtle rendering and execution risks. Production pipelines must account for these failure modes before scaling.
1. Blind Click Execution
Explanation: Dispatching clicks without verifying visibility triggers headless browser exceptions and leaves banners intact.
Fix: Always evaluate getBoundingClientRect() and computed display properties before clicking. Use page.waitForSelector() with visible: true when possible.
2. Static Selector Fragility
Explanation: Hardcoding exact class names or IDs breaks when CMPs push minor UI updates or run A/B tests.
Fix: Maintain fallback selectors per platform. Use attribute selectors ([data-testid], [aria-label]) where available, and pair them with container-level suppression.
3. Shadow DOM Blind Spots
Explanation: Modern CMPs encapsulate markup inside shadow roots. Standard document.querySelector() calls fail to reach these elements.
Fix: Use page.evaluate() with shadowRoot.querySelector() or leverage Playwright's locator() API which automatically pierces shadow boundaries.
4. Async Rendering Race Conditions
Explanation: Banners often load after networkidle or domcontentloaded events. Capturing too early results in missed overlays.
Fix: Implement a short polling loop or page.waitForFunction() that checks for banner presence before triggering dismissal. Add a 500-1000ms buffer after network idle.
5. Layout Collapse via Aggressive Hiding
Explanation: Setting display: none on a banner can cause parent containers to collapse, shifting content and breaking visual regression baselines.
Fix: Apply visibility: hidden and pointer-events: none as primary fallbacks. Reserve display: none for known overlay containers that don't affect document flow.
6. Overly Permissive Text Matching
Explanation: Broad regex patterns like /accept/i can match navigation links, form buttons, or analytics triggers, causing unintended navigation or state changes.
Fix: Anchor patterns to button roles and explicit consent phrasing. Combine with viewport position checks (banners typically render in the bottom 20% of the screen).
7. Multi-Step Consent Flows
Explanation: Some platforms require granular preference selection before showing the final accept button. Clicking prematurely fails.
Fix: Detect preference modals by checking for aria-modal="true" or specific class patterns. Apply a secondary dismissal pass after the initial interaction completes.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume capture API (>10k/day) | Rule-Based Dismiss Engine | Eliminates per-request fees and scales linearly with compute | Fixed infrastructure cost |
| Internal QA / Visual Regression | Rule-Based Dismiss Engine | Deterministic results prevent flaky test failures | Zero external dependency |
| Marketing OG Image Generation | Rule-Based Dismiss Engine | Guarantees clean social previews without manual intervention | Reduces design team overhead |
| Compliance Auditing / Legal | Manual CSS Overrides | Requires explicit control over exactly what is hidden/shown | High engineering time, low risk |
Configuration Template
// consent-dismiss-config.ts
import type { RuleRegistry } from './types';
export const CMP_RULES: RuleRegistry = [
{
platform: 'OneTrust',
interactionTargets: ['#onetrust-accept-btn-handler', '[data-cy="accept-all-cookies"]'],
suppressionContainers: ['#onetrust-banner-sdk', '#onetrust-consent-sdk'],
priority: 1
},
{
platform: 'Cookiebot',
interactionTargets: ['#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll', '.cookiebot-button-accept'],
suppressionContainers: ['#CybotCookiebotDialog'],
priority: 2
},
{
platform: 'Quantcast',
interactionTargets: ['#qc-cmp2-ui .qc-cmp-button-accept'],
suppressionContainers: ['#qc-cmp2-ui', '#qc-cmp2-root'],
priority: 3
},
{
platform: 'TrustArc',
interactionTargets: ['#truste-consent-button', '.truste-button-accept'],
suppressionContainers: ['#truste-consent-track'],
priority: 4
},
{
platform: 'Sourcepoint',
interactionTargets: ['.sp_message_container .accept-all-button'],
suppressionContainers: ['.sp_message_container'],
priority: 5
},
{
platform: 'Didomi',
interactionTargets: ['#didomi-notice-agree-button'],
suppressionContainers: ['#didomi-popup'],
priority: 6
},
{
platform: 'Iubenda',
interactionTargets: ['.iubenda-cs-accept-btn'],
suppressionContainers: ['.iubenda-cs-content', '.iubenda-cs-notice'],
priority: 7
},
{
platform: 'Usercentrics',
interactionTargets: ['#usercentrics-root .uc-btn'],
suppressionContainers: ['#usercentrics-root'],
priority: 8
},
{
platform: 'Borlabs',
interactionTargets: ['#borlabs-cookie-accept-all'],
suppressionContainers: ['#borlabs-cookie'],
priority: 9
},
{
platform: 'Complianz',
interactionTargets: ['#cmplz-accept-button'],
suppressionContainers: ['#cmplz-banner'],
priority: 10
},
{
platform: 'CookieYes',
interactionTargets: ['.cky-accept-btn'],
suppressionContainers: ['.cky-banner'],
priority: 11
},
{
platform: 'GDPR Plugin',
interactionTargets: ['#gdpr-cookie-notice-accept'],
suppressionContainers: ['#gdpr-cookie-notice'],
priority: 12
}
];
Quick Start Guide
- Initialize the engine: Import the rule registry and dismissal functions into your capture script. Ensure your headless browser instance supports
page.evaluate() and visibility APIs.
- Inject before capture: Call
executeDismissSequence(page, CMP_RULES) immediately after page.goto() and network stabilization. Add a 500ms buffer to allow async rendering.
- Validate output: Capture a test screenshot and verify that consent overlays are removed without content shifting. Check console logs for visibility guard triggers.
- Scale to pipeline: Wrap the dismissal call in a reusable middleware function. Apply it across all capture jobs, monitor false positive rates, and update the registry quarterly based on CMP release notes.