---
name: compliance-testing
description: >-
Test for regulatory compliance including GDPR/CMP consent verification, Better Ads
Standards, cookie compliance auditing, and privacy policy validation. Covers
automated consent flow testing, third-party script blocking before consent,
and cookie inventory validation. Use when: "GDPR test," "compliance," "CMP test,"
"cookie consent," "Better Ads," "privacy," "consent banner."
Related: accessibility-testing, security-testing, ci-cd-integration.
license: MIT
metadata:
author: kindlmann
version: "1.0"
category: process
---
Test applications for regulatory compliance, focusing on privacy regulations (GDPR, CCPA, ePrivacy), consent management, cookie governance, and advertising standards. Compliance is binary -- you either comply or you do not -- and the penalties for non-compliance are significant. Automated testing catches configuration drift and regressions that manual audits miss between review cycles.
**Before starting:** Check for `.agents/qa-project-context.md` in the project root. It contains applicable regulations, CMP details, ad networks, and geographic requirements that determine which compliance tests to implement.
---
## Discovery Questions
### Applicable Regulations
1. **Which privacy regulations apply?** GDPR (EU users), CCPA/CPRA (California), ePrivacy Directive (EU cookies), LGPD (Brazil), PIPEDA (Canada). Multiple regulations may apply simultaneously if you serve users in multiple regions.
2. **What is the legal basis for data processing?** Consent (opt-in), legitimate interest, contractual necessity? This determines whether explicit consent is required before processing.
3. **Is there a DPO or legal team to consult?** Compliance testing validates technical implementation against legal requirements. The legal team defines those requirements.
### Consent Management
4. **What CMP is in use?** OneTrust, Cookiebot, Didomi, Usercentrics, or custom? The CMP determines consent storage format, API, and integration patterns.
5. **What consent categories exist?** Typically: Strictly Necessary (always allowed), Analytics/Performance, Functional/Preferences, Marketing/Targeting.
6. **How is consent communicated to third-party scripts?** TCF (Transparency and Consent Framework)? Custom data layer? Direct CMP API?
### Advertising and Accessibility
7. **What ad networks and formats are used?** Google Ads, Meta, programmatic? Display, video, interstitial? The Coalition for Better Ads defines acceptable formats.
8. **Are there accessibility compliance requirements?** ADA, EAA, Section 508? See the `accessibility-testing` skill for detailed WCAG testing.
---
## Core Principles
### 1. Compliance Is Binary
There is no "mostly compliant." A cookie that fires before consent is a violation. A consent banner that cannot be dismissed without accepting is a violation. Test for exact compliance.
### 2. Automate What You Can, Audit What You Cannot
Automated tests catch: cookies set before consent, scripts loading without consent, consent banner functionality, cookie attributes, consent persistence. Manual audits catch: privacy policy accuracy, legal language correctness, cross-border data transfer documentation. Automate the technical checks; schedule the legal audits.
### 3. Test From the User Perspective
Compliance regulations are written from the user's perspective. Tests should simulate real user interactions with the consent flow, not just check backend state.
### 4. Regulations Change, Tests Must Be Updatable
New regulations emerge regularly. Structure compliance tests with configuration-driven test data so that changing a threshold or adding a cookie category does not require rewriting the suite.
### 5. Defense in Depth
Do not rely solely on the CMP. Verify at multiple layers: CMP configuration, CSP headers, script loading behavior, cookie state, and network requests.
---
## GDPR/CMP Testing with Playwright
### Consent Banner and Dark Pattern Checks
```typescript
import { test, expect } from '@playwright/test';
test.describe('GDPR Consent Banner', () => {
test.use({ storageState: undefined }); // Fresh context — first visit
test('consent banner appears on first visit', async ({ page }) => {
await page.goto('/');
const banner = page.getByRole('dialog', { name: /cookie|consent|privacy/i });
await expect(banner).toBeVisible({ timeout: 5000 });
});
test('banner provides accept and reject with equal prominence', async ({ page }) => {
await page.goto('/');
const banner = page.getByRole('dialog', { name: /cookie|consent|privacy/i });
const acceptBtn = banner.getByRole('button', { name: /accept|agree|allow/i });
const rejectBtn = banner.getByRole('button', { name: /reject|decline|deny/i });
await expect(acceptBtn).toBeVisible();
await expect(rejectBtn).toBeVisible();
// Dark pattern check: reject button must be reasonably sized, not a tiny link
const rejectBox = await rejectBtn.boundingBox();
expect(rejectBox).not.toBeNull();
expect(rejectBox!.width).toBeGreaterThan(60);
expect(rejectBox!.height).toBeGreaterThan(30);
});
test('banner links to privacy policy', async ({ page }) => {
await page.goto('/');
const banner = page.getByRole('dialog', { name: /cookie|consent|privacy/i });
const policyLink = banner.getByRole('link', { name: /privacy policy|learn more/i });
await expect(policyLink).toBeVisible();
await expect(policyLink).toHaveAttribute('href', /privacy/);
});
});
```
### Cookie State Before and After Consent
The critical test: no non-essential cookies may be set before the user gives consent.
```typescript
// Cookie classification helpers — maintain based on your cookie inventory
function isStrictlyNecessary(name: string): boolean {
const necessary = ['__Host-session', 'csrf_token', '__cf_bm', 'consent_status'];
return necessary.some((n) => name.startsWith(n));
}
function isAnalyticsCookie(name: string): boolean {
return ['_ga', '_gid', '_gat', '_gtag', 'analytics_'].some((p) => name.startsWith(p));
}
test.describe('Cookie Consent Compliance', () => {
test.use({ storageState: undefined });
test('no non-essential cookies before consent', async ({ page, context }) => {
await page.goto('/');
await expect(page.getByRole('dialog', { name: /cookie|consent/i })).toBeVisible();
const cookies = await context.cookies();
const violations = cookies.filter((c) => !isStrictlyNecessary(c.name));
expect(violations.map((c) => c.name), 'Non-essential cookies set before consent').toHaveLength(0);
});
test('analytics cookies appear after accepting consent', async ({ page, context }) => {
await page.goto('/');
const banner = page.getByRole('dialog', { name: /cookie|consent/i });
await banner.getByRole('button', { name: /accept|agree/i }).click();
await page.waitForTimeout(1000); // Allow async cookie setting
const cookies = await context.cookies();
expect(cookies.filter((c) => isAnalyticsCookie(c.name)).length).toBeGreaterThan(0);
});
test('no non-essential cookies after rejecting consent', async ({ page, context }) => {
await page.goto('/');
const banner = page.getByRole('dialog', { name: /cookie|consent/i });
await banner.getByRole('button', { name: /reject|decline|deny/i }).click();
await page.waitForTimeout(1000);
const cookies = await context.cookies();
const violations = cookies.filter((c) => !isStrictlyNecessary(c.name));
expect(violations.map((c) => c.name), 'Non-essential cookies after rejection').toHaveLength(0);
});
});
```
### Consent Persistence and Withdrawal
```typescript
test.describe('Consent Persistence', () => {
test.use({ storageState: undefined });
test('consent persists across navigations', async ({ page }) => {
await page.goto('/');
await page.getByRole('dialog', { name: /consent/i }).getByRole('button', { name: /accept/i }).click();
await page.goto('/about');
await expect(page.getByRole('dialog', { name: /consent/i })).toBeHidden({ timeout: 3000 });
});
test('user can withdraw consent via privacy settings', async ({ page, context }) => {
await page.goto('/');
await page.getByRole('dialog', { name: /consent/i }).getByRole('button', { name: /accept/i }).click();
await page.goto('/privacy-settings');
const analyticsToggle = page.getByRole('checkbox', { name: /analytics/i });
if (await analyticsToggle.isChecked()) await analyticsToggle.uncheck();
await page.getByRole('button', { name: /save/i }).click();
await page.waitForTimeout(1000);
const cookies = await context.cookies();
expect(cookies.filter((c) => isAnalyticsCookie(c.name))).toHaveLength(0);
});
});
```
### Third-Party Script Blocking Before Consent
The most critical compliance check: tracking scripts must not load before consent.
```typescript
test.describe('Script Blocking', () => {
test.use({ storageState: undefined });
test('no tracking scripts load before consent', async ({ page }) => {
const trackingDomains = [
'google-analytics.com', 'googletagmanager.com', 'facebook.net',
'connect.facebook.net', 'analytics.tiktok.com', 'bat.bing.com',
];
const violations: string[] = [];
page.on('request', (req) => {
const url = req.url();
if (trackingDomains.some((d) => url.includes(d))) violations.push(url);
});
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(violations, `Tracking scripts before consent:\n${violations.join('\n')}`).toHaveLength(0);
});
test('tracking scripts load after consent acceptance', async ({ page }) => {
const trackerLoaded: string[] = [];
page.on('request', (req) => {
if (/google-analytics|googletagmanager/.test(req.url())) trackerLoaded.push(req.url());
});
await page.goto('/');
await page.getByRole('dialog', { name: /consent/i }).getByRole('button', { name: /accept/i }).click();
await page.waitForTimeout(3000);
expect(trackerLoaded.length, 'Analytics should load after consent').toBeGreaterThan(0);
});
});
```
---
## Better Ads Standards
The Coalition for Better Ads defines ad formats that trigger browser-level ad blocking (Chrome filters ads on non-compliant sites).
### Unacceptable Ad Formats
| Format | Desktop | Mobile | Test Approach |
|--------|---------|--------|---------------|
| Pop-up ads | Yes | Yes | Check for modal/overlay within 5s of load without user action |
| Auto-playing video with sound | Yes | Yes | Monitor `