SuperAPI AI Directory

cookie-consent-testing

Testing & quality

View repository

Updated 10-16 01:31

About this skill

Automated Cookie Consent Validation

Overview

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.

Test Architecture

Test Categories

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

Test Environment Setup

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'] },
    },
  ],
});

Test Implementations

Test 1: Pre-Consent Cookie Blocking

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);
  });
});

Test 2: Banner Display and Interaction

// 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);
  });
});

Test 3: Consent State Verification

// 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);
  });
});

Test 4: Tag Firing Audit

// 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');
      }
    }
  });
});

Test 5: Cookie Regression Detection

// 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);
  });
});

Test 6: GPC Signal Handling

// 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();
  });
});

CI/CD Pipeline Integration

GitHub Actions Workflow

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 }}

Key Legal and Technical References

  • ePrivacy Directive 2002/58/EC, Article 5(3) — Legal requirement that automated tests verify
  • CJEU Case C-673/17 (Planet49) — Active consent requirements validated by banner interaction tests
  • CNIL Deliberation No. 2020-091 — Equal prominence requirement tested by button size comparison
  • CCPA/CPRA Cal. Civ. Code §1798.135(e) — GPC signal handling tested in GPC test suite
  • Playwright Documentation — Browser automation framework for cross-browser testing
  • WCAG 2.1 Level AA — Accessibility requirements tested by keyboard navigation tests
  • Google Consent Mode Documentation — Consent state parameters validated in tag firing tests