关于 cookie-consent-testing
面向 Web 开发与测试团队的 Cookie 同意自动化验证指南,利用 Selenium 或 Playwright 检查同意横幅、Cookie 阻止、标签触发和选择持久性,并介绍 CI/CD 集成。
主要功能
- 01验证首次访问时横幅显示
- 02检查同意前非必要 Cookie 是否被阻止
- 03测试接受、拒绝与分类选择
- 04审计标签触发和同意状态
- 05检测 Cookie 回归并验证 GPC
适用场景
- 01在每次部署时运行 Cookie 合规测试
- 02确认分析标签仅在获得同意后触发
- 03发现未经记录的新 Cookie
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mukul975/ privacy-data-protection-skills --skill cookie-consent-testing安装后在 Claude Code 中输入/cookie-consent-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/template.mdreferences/standards.mdworkflows.mdscripts/process.py详细介绍
自动化 Cookie 同意验证
概述
对于每天多次部署且第三方脚本独立更新的现代 Web 应用,仅靠手动测试 Cookie 同意是不够的。使用浏览器自动化框架(Selenium 和 Playwright)进行自动化测试,可以持续验证 Cookie 同意横幅是否正常运行、是否在用户同意前阻止非必要 Cookie、是否尊重用户的同意选择,以及标签触发是否与用户的同意状态一致。将这些测试集成到 CI/CD 流水线中,可确保每次部署在进入生产环境前都经过 Cookie 合规性验证。
测试架构
测试类别
| 类别 | 测试内容 | 运行时机 |
|---|---|---|
| 横幅显示 | 首次访问时显示横幅;布局和文本正确 | 每次部署 |
| 同意前阻止 | 用户交互前不设置非必要 Cookie/标签 | 每次部署 |
| 接受同意 | “全部接受”会设置正确的 Cookie 并触发正确的标签 | 每次部署 |
| 拒绝同意 | “全部拒绝”会阻止所有非必要 Cookie 和标签 | 每次部署 |
| 分类别同意 | 各类别的切换开关正常工作 | 每次部署 |
| 同意持久性 | 页面导航和浏览器重启后仍保留同意状态 | 每日 |
| 撤回同意 | 更改同意设置后会移除 Cookie 并停止标签 | 每周 |
| 同意过期 | 同意过期后横幅会重新显示 | 每月 |
| GPC 信号 | GPC 标头触发自动选择退出 | 每次部署 |
| 回归 | 未经文档记录不得引入新 Cookie | 每次部署 |
测试环境设置
Pinnacle E-Commerce Ltd 的 Playwright 配置:
// 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'] },
},
],
});
测试实现
测试 1:同意前阻止 Cookie
验证用户与同意横幅交互前,不会设置任何非必要 Cookie。
// 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);
});
});
测试 2:横幅显示与交互
// 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);
});
});
测试 3:同意状态验证
// 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);
});
});
测试 4:标签触发审计
// 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');
}
}
});
});
测试 5:Cookie 回归检测
// 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);
});
});
测试 6:GPC 信号处理
// 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 流水线集成
GitHub Actions 工作流
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 }}
重要法律和技术参考
- 《电子隐私指令》2002/58/EC,第 5(3) 条 — 自动化测试需要验证的法律要求
- 欧盟法院案件 C-673/17(Planet49) — 通过横幅交互测试验证主动同意要求
- CNIL 第 2020-091 号审议 — 通过比较按钮尺寸测试显著性相同的要求
- 《加州民法典》CCPA/CPRA §1798.135(e) — 在 GPC 测试套件中测试 GPC 信号处理
- Playwright 文档 — 用于跨浏览器测试的浏览器自动化框架
- WCAG 2.1 AA 级 — 通过键盘导航测试验证无障碍要求
- Google Consent Mode 文档 — 在标签触发测试中验证同意状态参数
评论
支持 Markdown