Automates cookie consent validation using Playwright and Selenium: tests banner interactions, consent states, tag firing audits, regression, and CI/CD integration for compliance.
npx claudepluginhub mukul975/privacy-data-protection-skills --plugin cookie-consent-skillsThis skill uses the workspace's default tool permissions.
Manual cookie consent testing is insufficient for modern web applications where deployments occur multiple times daily and third-party scripts update independently. Automated testing using browser automation frameworks — Selenium and Playwright — enables continuous verification that the cookie consent banner functions correctly, that non-essential cookies are blocked before consent, that consen...
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Manual cookie consent testing is insufficient for modern web applications where deployments occur multiple times daily and third-party scripts update independently. Automated testing using browser automation frameworks — Selenium and Playwright — enables continuous verification that the cookie consent banner functions correctly, that non-essential cookies are blocked before consent, that consent choices are respected, and that tag firing aligns with the user's consent state. Integrating these tests into the CI/CD pipeline ensures that every deployment is verified for cookie compliance before reaching production.
| Category | What It Tests | When to Run |
|---|---|---|
| Banner display | Banner appears on first visit; correct layout and text | Every deployment |
| Pre-consent blocking | No non-essential cookies/tags before user interaction | Every deployment |
| Consent acceptance | Accept All sets correct cookies and fires correct tags | Every deployment |
| Consent rejection | Reject All blocks all non-essential cookies and tags | Every deployment |
| Granular consent | Per-category toggles work correctly | Every deployment |
| Consent persistence | Consent state survives page navigation and browser restart | Daily |
| Consent withdrawal | Changing consent removes cookies and stops tags | Weekly |
| Consent expiry | Banner re-appears after consent expires | Monthly |
| GPC signal | GPC header triggers automatic opt-out | Every deployment |
| Regression | New cookies not introduced without documentation | Every deployment |
Playwright Configuration for Pinnacle E-Commerce Ltd:
// playwright.config.js
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests/cookie-consent',
timeout: 30000,
retries: 1,
use: {
baseURL: 'https://staging.pinnacle-ecommerce.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
],
});
Verify that no non-essential cookies are set before the user interacts with the consent banner.
// tests/cookie-consent/pre-consent-blocking.spec.js
const { test, expect } = require('@playwright/test');
const ESSENTIAL_COOKIES = [
'session_id',
'csrf_token',
'consent_state',
'load_balancer',
'pinnacle_consent_eu',
'pinnacle_consent_uk',
'pinnacle_consent_ccpa',
];
const NON_ESSENTIAL_COOKIES = [
'_ga',
'_ga_',
'_gid',
'_fbp',
'_fbc',
'_gcl_au',
'_hjSession',
'_hjSessionUser',
'IDE',
'fr',
'NID',
];
test.describe('Pre-Consent Cookie Blocking', () => {
test('no non-essential cookies are set on page load before consent', async ({ page }) => {
// Navigate to homepage without any prior consent
await page.goto('/');
// Wait for page to fully load including all third-party scripts
await page.waitForLoadState('networkidle');
// Get all cookies
const cookies = await page.context().cookies();
const cookieNames = cookies.map(c => c.name);
// Verify no non-essential cookies exist
for (const nonEssential of NON_ESSENTIAL_COOKIES) {
const found = cookieNames.filter(name => name.startsWith(nonEssential));
expect(found, `Non-essential cookie ${nonEssential} found before consent`).toHaveLength(0);
}
});
test('no analytics network requests before consent', async ({ page }) => {
const analyticsRequests = [];
// Monitor network requests
page.on('request', request => {
const url = request.url();
if (
url.includes('google-analytics.com') ||
url.includes('analytics.google.com') ||
url.includes('facebook.com/tr') ||
url.includes('connect.facebook.net') ||
url.includes('hotjar.com')
) {
analyticsRequests.push(url);
}
});
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(analyticsRequests, 'Analytics requests fired before consent').toHaveLength(0);
});
test('no localStorage tracking entries before consent', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const trackingKeys = await page.evaluate(() => {
const suspicious = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('_hj') || key.startsWith('_ga') || key.includes('fb_')) {
suspicious.push(key);
}
}
return suspicious;
});
expect(trackingKeys, 'Tracking localStorage entries found before consent').toHaveLength(0);
});
});
// tests/cookie-consent/banner-display.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Cookie Banner Display', () => {
test('banner appears on first visit', async ({ page }) => {
await page.goto('/');
// Banner should be visible
const banner = page.locator('[data-testid="cookie-banner"]');
await expect(banner).toBeVisible();
});
test('banner has Accept All button', async ({ page }) => {
await page.goto('/');
const acceptButton = page.locator('[data-testid="cookie-accept-all"]');
await expect(acceptButton).toBeVisible();
await expect(acceptButton).toHaveText(/Accept All/i);
});
test('banner has Reject All button with equal prominence', async ({ page }) => {
await page.goto('/');
const acceptButton = page.locator('[data-testid="cookie-accept-all"]');
const rejectButton = page.locator('[data-testid="cookie-reject-all"]');
await expect(rejectButton).toBeVisible();
await expect(rejectButton).toHaveText(/Reject All/i);
// Verify equal visual prominence (same size)
const acceptBox = await acceptButton.boundingBox();
const rejectBox = await rejectButton.boundingBox();
expect(Math.abs(acceptBox.width - rejectBox.width)).toBeLessThan(10);
expect(Math.abs(acceptBox.height - rejectBox.height)).toBeLessThan(5);
});
test('banner has Customise/Manage Preferences option', async ({ page }) => {
await page.goto('/');
const customiseButton = page.locator('[data-testid="cookie-customise"]');
await expect(customiseButton).toBeVisible();
});
test('banner does not reappear after making a choice', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="cookie-accept-all"]').click();
// Navigate to another page
await page.goto('/products');
await page.waitForLoadState('networkidle');
const banner = page.locator('[data-testid="cookie-banner"]');
await expect(banner).not.toBeVisible();
});
test('banner is keyboard accessible', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
// Focus should be within the banner
const focusedElement = await page.evaluate(() => {
const el = document.activeElement;
return el ? el.closest('[data-testid="cookie-banner"]') !== null : false;
});
expect(focusedElement).toBe(true);
});
});
// tests/cookie-consent/consent-state.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Consent State Verification', () => {
test('Accept All sets all consent categories to granted', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="cookie-accept-all"]').click();
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies();
const consentCookie = cookies.find(c => c.name === 'pinnacle_consent_eu');
expect(consentCookie).toBeTruthy();
const consentState = JSON.parse(decodeURIComponent(consentCookie.value));
expect(consentState.analytics).toBe(true);
expect(consentState.advertising).toBe(true);
expect(consentState.functionality).toBe(true);
});
test('Reject All sets all consent categories to denied', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="cookie-reject-all"]').click();
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies();
const consentCookie = cookies.find(c => c.name === 'pinnacle_consent_eu');
expect(consentCookie).toBeTruthy();
const consentState = JSON.parse(decodeURIComponent(consentCookie.value));
expect(consentState.analytics).toBe(false);
expect(consentState.advertising).toBe(false);
expect(consentState.functionality).toBe(false);
});
test('Reject All blocks GA4 cookies', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="cookie-reject-all"]').click();
await page.waitForLoadState('networkidle');
// Navigate to multiple pages to give GA4 time to attempt cookie setting
await page.goto('/products');
await page.waitForLoadState('networkidle');
await page.goto('/about');
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies();
const gaCookies = cookies.filter(c => c.name.startsWith('_ga'));
expect(gaCookies).toHaveLength(0);
});
test('Granular consent: analytics only sets only analytics cookies', async ({ page }) => {
await page.goto('/');
// Open customisation layer
await page.locator('[data-testid="cookie-customise"]').click();
// Enable only analytics
const analyticsToggle = page.locator('[data-testid="consent-toggle-analytics"]');
await analyticsToggle.click();
// Ensure advertising is off
const advertisingToggle = page.locator('[data-testid="consent-toggle-advertising"]');
const isAdvertisingChecked = await advertisingToggle.isChecked();
expect(isAdvertisingChecked).toBe(false);
// Confirm choices
await page.locator('[data-testid="cookie-confirm-choices"]').click();
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies();
const cookieNames = cookies.map(c => c.name);
// GA cookies should be present
expect(cookieNames.some(n => n.startsWith('_ga'))).toBe(true);
// Advertising cookies should NOT be present
expect(cookieNames.some(n => n === '_fbp')).toBe(false);
expect(cookieNames.some(n => n === '_gcl_au')).toBe(false);
});
});
// tests/cookie-consent/tag-firing.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Tag Firing Audit', () => {
test('GA4 tag fires only after analytics consent', async ({ page }) => {
let ga4Fired = false;
page.on('request', request => {
if (request.url().includes('google-analytics.com/g/collect') ||
request.url().includes('analytics.google.com/g/collect')) {
ga4Fired = true;
}
});
// Load page — GA4 should not fire
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(ga4Fired, 'GA4 fired before consent').toBe(false);
// Accept analytics
await page.locator('[data-testid="cookie-customise"]').click();
await page.locator('[data-testid="consent-toggle-analytics"]').click();
await page.locator('[data-testid="cookie-confirm-choices"]').click();
await page.waitForLoadState('networkidle');
// Navigate to trigger a pageview
await page.goto('/products');
await page.waitForLoadState('networkidle');
expect(ga4Fired, 'GA4 did not fire after analytics consent').toBe(true);
});
test('Meta Pixel does not fire when advertising is rejected', async ({ page }) => {
let metaPixelFired = false;
page.on('request', request => {
if (request.url().includes('facebook.com/tr') ||
request.url().includes('connect.facebook.net')) {
metaPixelFired = true;
}
});
await page.goto('/');
await page.locator('[data-testid="cookie-reject-all"]').click();
await page.waitForLoadState('networkidle');
await page.goto('/products');
await page.waitForLoadState('networkidle');
await page.goto('/products/widget-pro');
await page.waitForLoadState('networkidle');
expect(metaPixelFired, 'Meta Pixel fired after reject all').toBe(false);
});
test('Google Consent Mode sends correct consent state', async ({ page }) => {
const consentPings = [];
page.on('request', request => {
const url = request.url();
if (url.includes('google-analytics.com') && url.includes('gcs=')) {
const gcsMatch = url.match(/gcs=([^&]+)/);
if (gcsMatch) consentPings.push(gcsMatch[1]);
}
});
await page.goto('/');
await page.locator('[data-testid="cookie-reject-all"]').click();
await page.waitForLoadState('networkidle');
// Navigate to trigger consent mode ping
await page.goto('/products');
await page.waitForLoadState('networkidle');
// If consent mode pings are sent, verify denied state
if (consentPings.length > 0) {
for (const gcs of consentPings) {
// G100 = all denied, G111 = all granted
expect(gcs).toContain('100');
}
}
});
});
// tests/cookie-consent/regression.spec.js
const { test, expect } = require('@playwright/test');
const fs = require('fs');
// Baseline of known cookies — update when new cookies are intentionally added
const KNOWN_COOKIES = new Set([
'session_id',
'csrf_token',
'consent_state',
'pinnacle_consent_eu',
'load_balancer',
'_ga',
'_ga_PINNACLE',
'_gid',
'_fbp',
'_fbc',
'_gcl_au',
'_hjSessionUser',
'_hjSession',
'locale',
'currency',
'recently_viewed',
'cart_session',
'auth_token',
]);
test.describe('Cookie Regression Detection', () => {
test('no unknown cookies after Accept All', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="cookie-accept-all"]').click();
await page.waitForLoadState('networkidle');
// Visit several pages to trigger all tag scenarios
const pages = ['/', '/products', '/products/widget-pro', '/cart', '/about', '/blog'];
for (const path of pages) {
await page.goto(path);
await page.waitForLoadState('networkidle');
}
const cookies = await page.context().cookies();
const unknownCookies = cookies.filter(c => {
// Check if cookie name matches any known prefix
return !Array.from(KNOWN_COOKIES).some(known =>
c.name === known || c.name.startsWith(known)
);
});
if (unknownCookies.length > 0) {
const unknownNames = unknownCookies.map(c => `${c.name} (domain: ${c.domain}, expires: ${c.expires})`);
console.error('Unknown cookies detected:', unknownNames);
// Write to report file for review
fs.writeFileSync(
'test-results/unknown-cookies.json',
JSON.stringify(unknownCookies, null, 2)
);
}
expect(unknownCookies, `Unknown cookies found: ${unknownCookies.map(c => c.name).join(', ')}`).toHaveLength(0);
});
});
// tests/cookie-consent/gpc-signal.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Global Privacy Control Signal', () => {
test('GPC signal triggers automatic opt-out for California users', async ({ browser }) => {
// Create context with GPC header
const context = await browser.newContext({
extraHTTPHeaders: {
'Sec-GPC': '1',
},
locale: 'en-US',
geolocation: { latitude: 34.0522, longitude: -118.2437 }, // Los Angeles
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('/');
await page.waitForLoadState('networkidle');
// Verify advertising cookies are not set
const cookies = await context.cookies();
const adCookies = cookies.filter(c =>
c.name === '_fbp' || c.name === '_gcl_au' || c.name.startsWith('IDE')
);
expect(adCookies, 'Advertising cookies set despite GPC signal').toHaveLength(0);
await context.close();
});
});
name: Cookie Consent Compliance Tests
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM UTC
jobs:
cookie-consent-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run cookie consent tests
run: npx playwright test tests/cookie-consent/
env:
BASE_URL: ${{ vars.STAGING_URL }}
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: cookie-consent-test-results
path: |
test-results/
playwright-report/
- name: Post results to Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Cookie consent tests FAILED on ${{ github.ref }}. Review: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_PRIVACY_WEBHOOK }}