mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
components/consent/Consent.tsx and Choices.tsx are gone. app/providers.tsx mounts Consent from @hanzo/ui/consent 8.29.14: a bar with Manage preferences, Accept all and a × that stores essential cookies only, and a Cookie Preferences sheet that opens from any link to #choices, the footer's Privacy choices included. Its switches start off until chosen. @hanzo/event 0.3.61 makes the stream first party: every page view and event goes for every visitor, stating the grants it went under, so the provider no longer takes `enabled` from consent. Consent gates Google and Meta alone, through startTags. The conversion and forward gates now hold that no platform hears of a visit before Accept or after a refusal, while our stream counts it under no grant. Help's corner words gain the Font and Arabic font rows @hanzo/ui 8.29.12 added to Words.
420 lines
23 KiB
TypeScript
420 lines
23 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
||
import { secure, serveExport } from './export'
|
||
import { serve } from './api'
|
||
import { config, ga, heard, person, stream } from './heard'
|
||
|
||
/**
|
||
* A CONVERSION, COUNTED ONCE IN EVERY PLACE IT IS COUNTED.
|
||
*
|
||
* Every funnel moment goes through one call (lib/analytics/tags.ts track) that
|
||
* reaches our stream, GA4 and Meta under one event_id, with the visitor's
|
||
* consent and the ad click they came through. cloud forwards the stream's copy
|
||
* server-side (apps/destination), so these hold the browser half of that
|
||
* contract against `out/`, the bytes that ship:
|
||
*
|
||
* the click is captured first-party, and every event carries it;
|
||
* GA4 is sent each event exactly once — by gtag when gtag is running, and
|
||
* otherwise by nobody in the browser, so cloud sends it instead;
|
||
* our own stream is first party and counts every visitor from the first page,
|
||
* whatever they chose; each event states the grants it went under;
|
||
* every visitor, whatever their clock says, is asked first: nothing reaches a
|
||
* platform (Google, Meta) before Accept, and nothing after the bar's ×, which
|
||
* is the refusal (essential cookies only);
|
||
* Cookie settings open from `#choices`, all off until chosen, and a choice
|
||
* decides what loads;
|
||
* Global Privacy Control and Do Not Track hold every platform off.
|
||
*
|
||
* The network is ours: gtag.js and fbevents.js are answered with stand-ins, and
|
||
* /v1/event is recorded rather than sent.
|
||
*/
|
||
|
||
const GA = 'G-RBVVVLFZDR'
|
||
|
||
/** The sheet's three switches (@hanzo/ui/consent): analytics, marketing, ads. */
|
||
const PERFORMANCE = 'Performance Cookies'
|
||
const MARKETING = 'Marketing Cookies'
|
||
const ADS = 'Sell, Share, or Use Your Data for Targeted Advertising'
|
||
const OPTIONAL = [PERFORMANCE, MARKETING, ADS]
|
||
|
||
/** Answers the two ad libraries. `live` stands gtag.js up the way it would be. */
|
||
async function tags(page: Page, live: boolean): Promise<string[]> {
|
||
const fetched: string[] = []
|
||
// The plans are the plan authority's, read when /pricing opens (e2e/gates/api).
|
||
await serve(page)
|
||
await person(page)
|
||
await config(page)
|
||
await page.route('https://www.googletagmanager.com/**', (route) => {
|
||
fetched.push('gtag')
|
||
return live
|
||
? route.fulfill({ status: 200, contentType: 'text/javascript', body: `window.google_tag_manager={'${GA}':{}}` })
|
||
: route.abort()
|
||
})
|
||
await page.route('https://connect.facebook.net/**', (route) => {
|
||
fetched.push('fbevents')
|
||
return route.fulfill({ status: 200, contentType: 'text/javascript', body: '' })
|
||
})
|
||
await page.route(/pay\.hanzo\.ai/, (route) => route.fulfill({ status: 200, contentType: 'text/html', body: '<title>pay</title>' }))
|
||
if (live) await page.addInitScript((ga) => ((window as unknown as Record<string, unknown>).google_tag_manager = { [ga]: {} }), GA)
|
||
return fetched
|
||
}
|
||
|
||
/** The Meta calls the Pixel queued: [command, name, params, options]. */
|
||
const meta = (page: Page) =>
|
||
page.evaluate(() =>
|
||
((window as unknown as { fbq?: { queue?: IArguments[] } }).fbq?.queue ?? []).map((a) => Array.from(a)),
|
||
)
|
||
|
||
test.describe('conversions', () => {
|
||
// A visitor who accepted everything (playwright.config ANSWERED); the cookie
|
||
// question itself starts from a browser that has answered none, below.
|
||
let base: string
|
||
let stop: () => Promise<void>
|
||
test.beforeAll(async () => {
|
||
const served = await serveExport()
|
||
base = served.url
|
||
stop = served.close
|
||
})
|
||
test.afterAll(async () => stop())
|
||
|
||
test('a moment fired before gtag.js answers waits for it, and then goes through gtag', async ({ page }) => {
|
||
await serve(page)
|
||
await person(page)
|
||
const seen = await stream(page)
|
||
// gtag.js arrives a second and a half after it is asked for, as a real one
|
||
// on a slow line does. pricing_viewed fires at hydration, before that.
|
||
await page.route('https://www.googletagmanager.com/**', async (route) => {
|
||
await new Promise((r) => setTimeout(r, 1500))
|
||
return route.fulfill({ status: 200, contentType: 'text/javascript', body: `window.google_tag_manager={'${GA}':{}}` })
|
||
})
|
||
await page.route('https://connect.facebook.net/**', (route) =>
|
||
route.fulfill({ status: 200, contentType: 'text/javascript', body: '' }),
|
||
)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
|
||
const view = await heard(seen, 'pricing_viewed')
|
||
expect(view.tags, 'held for gtag, so the browser took the GA4 path').toBe('ga,meta')
|
||
const list = (await ga(page)).find(([name]) => name === 'view_item_list')
|
||
expect(list?.[1].event_id).toBe(view.event_id)
|
||
})
|
||
|
||
test('a landing click is kept first-party, and the list view carries it under one event_id', async ({ page, context }) => {
|
||
const seen = await stream(page)
|
||
await tags(page, true)
|
||
await page.goto(`${base}/pricing?gclid=G1&fbclid=F1&utm_source=google&utm_medium=cpc`, { waitUntil: 'load' })
|
||
|
||
const view = await heard(seen, 'pricing_viewed')
|
||
const jar = Object.fromEntries((await context.cookies()).map((c) => [c.name, decodeURIComponent(c.value)]))
|
||
expect(JSON.parse(jar.hz_touch)).toMatchObject({ gclid: 'G1', fbclid: 'F1' })
|
||
expect(jar._fbc).toMatch(/^fb\.1\.\d{13}\.F1$/)
|
||
expect(jar._fbp).toMatch(/^fb\.1\.\d{13}\.\d+$/)
|
||
expect(view).toMatchObject({
|
||
gclid: 'G1',
|
||
fbclid: 'F1',
|
||
fbc: jar._fbc,
|
||
fbp: jar._fbp,
|
||
consent: 'analytics,marketing,ads',
|
||
tags: 'ga,meta',
|
||
})
|
||
expect(view.cid, 'a GA4 client id, minted first-party when _ga is absent').toMatch(/^\d+\.\d+$/)
|
||
expect(typeof view.clicked).toBe('number')
|
||
expect((view.items as { item_id: string }[])[0].item_id).toBeTruthy()
|
||
|
||
const id = view.event_id
|
||
expect(id).toMatch(/^[0-9a-f-]{36}$/)
|
||
const list = (await ga(page)).find(([name]) => name === 'view_item_list')
|
||
expect(list?.[1].event_id, 'GA4 heard the same moment').toBe(id)
|
||
const content = (await meta(page)).find((c) => c[0] === 'track' && c[1] === 'ViewContent')
|
||
expect((content?.[3] as { eventID?: string })?.eventID, 'Meta heard it under the same id').toBe(id)
|
||
})
|
||
|
||
test('the plan CTA is a selection and a checkout, each one event_id everywhere', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
await tags(page, true)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
// The view is reported by an effect, so once it is heard the page is hydrated
|
||
// and the CTA's own handler is attached; a press before that is the bare link.
|
||
await heard(seen, 'pricing_viewed')
|
||
// Sign-up first, then pay: a card plan's CTA is /signup carrying its checkout
|
||
// (components/pricing/Plans.tsx `bought`).
|
||
const cta = page.locator('main a[data-ga][href*="pay%2Fconfirm"]').first()
|
||
await expect(cta).toBeVisible({ timeout: 15_000 })
|
||
await page.evaluate(() => document.addEventListener('click', (e) => e.preventDefault(), { capture: true, once: true }))
|
||
// The anchor's own click: what a press on it sends, whatever the layout lays over it.
|
||
await cta.evaluate((a) => (a as HTMLElement).click())
|
||
|
||
const pick = await heard(seen, 'plan_clicked')
|
||
const buy = await heard(seen, 'checkout_started')
|
||
expect(buy).toMatchObject({ currency: 'USD', tags: 'ga,meta' })
|
||
expect(typeof buy.value).toBe('number')
|
||
const events = await ga(page)
|
||
expect(events.filter(([n]) => n === 'select_item').map(([, p]) => p.event_id)).toEqual([pick.event_id])
|
||
expect(events.filter(([n]) => n === 'begin_checkout').map(([, p]) => p.event_id), 'one begin_checkout, not one per listener').toEqual([buy.event_id])
|
||
const initiate = (await meta(page)).filter((c) => c[1] === 'InitiateCheckout')
|
||
expect(initiate.map((c) => (c[3] as { eventID: string }).eventID)).toEqual([buy.event_id])
|
||
expect(seen.filter((e) => e.event === 'checkout_started'), 'the click-out handler left it to the CTA').toHaveLength(1)
|
||
})
|
||
|
||
test('with gtag.js blocked nothing is queued for it, and the event says so for the server', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
await tags(page, false)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
const view = await heard(seen, 'pricing_viewed')
|
||
expect(view.tags, 'gtag never ran, so GA4 is the server\'s to send').toBe('meta')
|
||
expect(await ga(page), 'nothing waits in dataLayer to double-count later').toEqual([])
|
||
})
|
||
|
||
test.describe('the cookie question', () => {
|
||
// On the real host, over https: the page the reader gets, whose choice is kept
|
||
// under __Host- names (export.ts secure).
|
||
test.use({ storageState: { cookies: [], origins: [] }, timezoneId: 'America/Los_Angeles' })
|
||
let local: string
|
||
test.beforeEach(async ({ page }) => {
|
||
local = base
|
||
base = await secure(page, local)
|
||
})
|
||
test.afterEach(() => {
|
||
base = local
|
||
})
|
||
|
||
/** Every request the page makes to a third party that measures it. Our own stream is not one. */
|
||
const MEASURES = /googletagmanager|google-analytics|doubleclick|googleadservices|facebook\.(net|com)|licdn|linkedin|ads-twitter|tiktok|redditstatic|bing\.com/
|
||
const watch = (page: Page): string[] => {
|
||
const out: string[] = []
|
||
page.on('request', (r) => {
|
||
if (MEASURES.test(r.url())) out.push(r.url())
|
||
})
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* What a page may not keep before a choice, nor after a refusal: a platform's ids
|
||
* (_ga*, _gid, _gat*, _gcl*, _gac*, _fb*, _tt*, ttcsid*, _twclid, li_*, _uet*) and
|
||
* the two of ours kept for one (hz_cid for Google Analytics, hz_touch the ad
|
||
* click). Our stream's own id, session and touches are first party and stay.
|
||
*/
|
||
const MEASURING = /^(hz_cid|hz_touch|_ga|_gid|_gat|_gcl|_gac|_fb|_tt|ttcsid|_twclid|twclid|li_|_uet|_scid|_rdt)/
|
||
/** What a visitor typed, held for the trip to sign in (components/shell/prompt.ts): theirs, not a measure. */
|
||
const THEIRS = new Set(['hz_prompt'])
|
||
const measuring = (names: string[]) => names.filter((n) => MEASURING.test(n) && !THEIRS.has(n))
|
||
|
||
/** What the page keeps on the device: cookies, localStorage and sessionStorage keys. */
|
||
const kept = async (page: Page) => ({
|
||
cookies: measuring((await page.context().cookies()).map((c) => c.name)),
|
||
local: measuring(await page.evaluate(() => Object.keys(window.localStorage))),
|
||
session: measuring(await page.evaluate(() => Object.keys(window.sessionStorage))),
|
||
})
|
||
|
||
/** The stream's events that went under a grant: none, for a visitor who allowed nothing. */
|
||
const granted = (seen: { properties?: Record<string, unknown> }[]) => seen.filter((e) => String(e.properties?.consent ?? '') !== '')
|
||
|
||
/**
|
||
* No platform heard of the visit and nothing of theirs was kept on the device, while
|
||
* our own stream counted it under no grant: waited for past one flush of the client
|
||
* (5 s), not just looked at.
|
||
*/
|
||
const silent = async (page: Page, seen: { event?: string; properties?: Record<string, unknown> }[], fetched: string[], measured: string[]) => {
|
||
await page.waitForTimeout(6_000)
|
||
expect(fetched, 'nothing from a platform').toEqual([])
|
||
expect(measured, 'no request to a platform').toEqual([])
|
||
expect(await kept(page), 'nothing of a platform kept').toEqual({ cookies: [], local: [], session: [] })
|
||
expect(seen.map((e) => e.event), 'our own stream counts the visit').toContain('$pageview')
|
||
expect(granted(seen), 'and states no grant').toEqual([])
|
||
}
|
||
|
||
test('a visitor on a US clock is asked, and no platform loads or hears until they accept', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
const banner = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
await expect(banner).toBeVisible({ timeout: 15_000 })
|
||
await silent(page, seen, fetched, measured)
|
||
await banner.getByRole('button', { name: 'Accept all' }).click()
|
||
await expect.poll(() => fetched, { message: 'accepting loads the tags with no reload' }).toContain('gtag')
|
||
expect(fetched).toContain('fbevents')
|
||
const view = await heard(seen, 'pricing_viewed', (p) => p.consent === 'analytics,marketing,ads')
|
||
expect(view.consent).toBe('analytics,marketing,ads')
|
||
await heard(seen, '$pageview')
|
||
await page.waitForTimeout(6_000)
|
||
expect(seen.filter((e) => e.event === '$pageview' && (e as { path?: string }).path === '/pricing'), 'the view is counted once').toHaveLength(1)
|
||
})
|
||
|
||
test('the bar\'s × is a refusal: essential cookies only, nothing sent, nothing loaded', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
const banner = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
await expect(banner).toBeVisible({ timeout: 15_000 })
|
||
await banner.getByRole('button', { name: 'Close and use essential cookies only' }).click()
|
||
await expect(banner).toHaveCount(0)
|
||
await silent(page, seen, fetched, measured)
|
||
const choice = (await page.context().cookies()).filter((c) => c.name.startsWith('__Host-hz_consent'))
|
||
expect(choice.map((c) => [c.name, c.value, c.secure, c.domain]), 'the choice is the host\'s own').toEqual([
|
||
['__Host-hz_consent', '', true, 'hanzo.ai'],
|
||
['__Host-hz_consent_v', '2', true, 'hanzo.ai'],
|
||
])
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' }), 'a choice is not asked again').toHaveCount(0)
|
||
await silent(page, seen, fetched, measured)
|
||
})
|
||
|
||
for (const when of ['after the page settled', 'within one flush of the client'] as const) {
|
||
test(`a visitor who accepts and then switches everything off ${when} is measured no more: the page reloads itself, and keeps nothing`, async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
await page.goto(`${base}/pricing?gclid=G1&fbclid=F1&utm_source=google`, { waitUntil: 'load' })
|
||
await page.getByRole('dialog', { name: 'Cookie preferences' }).getByRole('button', { name: 'Accept all' }).click()
|
||
await expect.poll(() => fetched, { message: 'Accept loads the libraries' }).toContain('fbevents')
|
||
if (when === 'after the page settled') {
|
||
await heard(seen, 'pricing_viewed', (p) => p.consent === 'analytics,marketing,ads')
|
||
await page.waitForTimeout(6_000)
|
||
}
|
||
// Everything from here on is after the visitor's refusal, on the same page and then the next.
|
||
const mark = { seen: seen.length, fetched: fetched.length, measured: measured.length }
|
||
await page.locator('footer[data-slot="footer"] a[href="#choices"]').click()
|
||
const panel = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
for (const name of OPTIONAL) await panel.getByRole('switch', { name, exact: true }).uncheck()
|
||
const reloaded = page.waitForEvent('load')
|
||
await panel.getByRole('button', { name: 'Save preferences' }).click()
|
||
await reloaded
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' }), 'the choice holds').toHaveCount(0)
|
||
await page.waitForTimeout(6_000)
|
||
expect(granted(seen.slice(mark.seen)), 'our own stream goes on, under no grant').toEqual([])
|
||
expect(fetched.slice(mark.fetched), 'no platform library after the refusal').toEqual([])
|
||
expect(measured.slice(mark.measured), 'no request to a platform after the refusal').toEqual([])
|
||
expect(await kept(page), 'nothing of a platform kept').toEqual({ cookies: [], local: [], session: [] })
|
||
})
|
||
}
|
||
|
||
test('a visitor who withdraws Marketing alone within one flush sends no ad platform anything more', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
await page.goto(`${base}/pricing?gclid=G1&fbclid=F1&utm_source=google`, { waitUntil: 'load' })
|
||
await page.getByRole('dialog', { name: 'Cookie preferences' }).getByRole('button', { name: 'Accept all' }).click()
|
||
await expect.poll(() => fetched, { message: 'Accept loads the libraries' }).toContain('fbevents')
|
||
const mark = { seen: seen.length, fetched: fetched.length }
|
||
await page.locator('footer[data-slot="footer"] a[href="#choices"]').click()
|
||
const panel = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
await panel.getByRole('switch', { name: MARKETING, exact: true }).uncheck()
|
||
await panel.getByRole('switch', { name: ADS, exact: true }).uncheck()
|
||
const reloaded = page.waitForEvent('load')
|
||
await panel.getByRole('button', { name: 'Save preferences' }).click()
|
||
await reloaded
|
||
await page.waitForTimeout(6_000)
|
||
const after = seen.slice(mark.seen)
|
||
expect(after.filter((e) => String(e.properties?.consent ?? '').includes('marketing')), 'nothing sent under the withdrawn grant').toEqual([])
|
||
expect(after.some((e) => e.properties?.consent === 'analytics'), 'Analytics goes on').toBe(true)
|
||
expect(fetched.slice(mark.fetched), 'no ad library after the withdrawal').not.toContain('fbevents')
|
||
const ids = (await page.context().cookies()).map((c) => c.name).filter((n) => /^(_fb|_gcl|hz_touch)/.test(n))
|
||
expect(ids, 'no ad id kept').toEqual([])
|
||
})
|
||
|
||
test('a refusal cloud saw on the request (Sec-GPC) is a refusal: not asked, no platform hears', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
// Registered last, so it answers first: cloud's policy for a request that carried Sec-GPC.
|
||
await page.route(
|
||
(url) => url.pathname === '/v1/project/tags',
|
||
(route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
headers: { 'access-control-allow-origin': '*' },
|
||
body: JSON.stringify({
|
||
tags: [{ platform: 'ga4', type: 'ga', id: GA }, { platform: 'meta', type: 'meta', id: '1790308611893001' }],
|
||
consent: { region: 'US', mode: 'opt-in', version: 2, refused: true, defaults: { analytics: false, marketing: false, ads: false } },
|
||
}),
|
||
}),
|
||
)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await page.waitForTimeout(2_000)
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' }), 'the browser already answered').toHaveCount(0)
|
||
await silent(page, seen, fetched, measured)
|
||
})
|
||
|
||
test('Cookie settings open from #choices, all off, and a choice decides what loads', async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
await page.goto(`${base}/pricing#choices`, { waitUntil: 'load' })
|
||
const panel = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
await expect(panel).toBeVisible({ timeout: 15_000 })
|
||
for (const name of OPTIONAL)
|
||
await expect(panel.getByRole('switch', { name, exact: true }), `${name} starts off`).not.toBeChecked()
|
||
|
||
await panel.getByRole('switch', { name: PERFORMANCE, exact: true }).check()
|
||
await panel.getByRole('switch', { name: ADS, exact: true }).check()
|
||
await panel.getByRole('button', { name: 'Save preferences' }).click()
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' }), 'saving closes it').toHaveCount(0)
|
||
|
||
// The next page load reads the stored choice.
|
||
fetched.length = 0
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
const view = await heard(seen, 'pricing_viewed', (p) => p.consent === 'analytics,ads')
|
||
expect(view.consent).toBe('analytics,ads')
|
||
expect(view.gclid ?? view.fbp, 'no ad ids without Marketing').toBeUndefined()
|
||
expect(fetched, 'the Pixel is never fetched without Marketing').not.toContain('fbevents')
|
||
expect(await page.evaluate(() => typeof (window as unknown as { fbq?: unknown }).fbq)).toBe('undefined')
|
||
const consent = await page.evaluate(() =>
|
||
((window as unknown as { dataLayer: IArguments[] }).dataLayer ?? [])
|
||
.map((a) => Array.from(a))
|
||
.find((a) => a[0] === 'consent' && a[1] === 'default')?.[2],
|
||
)
|
||
expect(consent).toMatchObject({ analytics_storage: 'denied', ad_storage: 'denied' })
|
||
})
|
||
|
||
test('a choice made under the opt-out policy, or set by another host, is asked again, and counts for nothing meanwhile', async ({ page, context }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
// The old policy's yes under this host's own names, and a current-looking yes
|
||
// under the bare names a sibling host could have set: neither is a choice.
|
||
await context.addCookies([
|
||
...[['__Host-hz_consent', 'analytics,marketing,ads'], ['__Host-hz_consent_v', '1']].map(([name, value]) => ({ name, value, url: `${base}/`, secure: true })),
|
||
...[['hz_consent', 'analytics,marketing,ads'], ['hz_consent_v', '2']].map(([name, value]) => ({ name, value, domain: '.hanzo.ai', path: '/' })),
|
||
])
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' })).toBeVisible({ timeout: 15_000 })
|
||
await page.waitForTimeout(6_000)
|
||
expect(fetched).toEqual([])
|
||
expect(measured).toEqual([])
|
||
expect(granted(seen), 'our own stream counts the visit under no grant').toEqual([])
|
||
})
|
||
|
||
test('the footer row is the control', async ({ page }) => {
|
||
await tags(page, true)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await page.getByRole('dialog', { name: 'Cookie preferences' }).getByRole('button', { name: 'Close and use essential cookies only' }).click()
|
||
await page.locator('footer[data-slot="footer"] a[href="#choices"]').click()
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' })).toBeVisible()
|
||
})
|
||
|
||
for (const [signal, script] of [
|
||
['Global Privacy Control', () => Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => true })],
|
||
['Do Not Track', () => Object.defineProperty(Navigator.prototype, 'doNotTrack', { get: () => '1' })],
|
||
] as const) {
|
||
test(`${signal} is a refusal: not asked, no platform hears, every switch held off`, async ({ page }) => {
|
||
const seen = await stream(page)
|
||
const fetched = await tags(page, true)
|
||
const measured = watch(page)
|
||
await page.addInitScript(script)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await expect(page.getByRole('dialog', { name: 'Cookie preferences' }), 'the browser already answered').toHaveCount(0)
|
||
await silent(page, seen, fetched, measured)
|
||
await page.goto(`${base}/pricing#choices`, { waitUntil: 'load' })
|
||
const panel = page.getByRole('dialog', { name: 'Cookie preferences' })
|
||
await expect(panel).toBeVisible({ timeout: 15_000 })
|
||
for (const name of OPTIONAL) {
|
||
const on = panel.getByRole('switch', { name, exact: true })
|
||
await expect(on).not.toBeChecked()
|
||
await expect(on, `${name} cannot be switched on`).toBeDisabled()
|
||
}
|
||
await expect(panel.getByText(/asks sites not to track you/)).toBeVisible()
|
||
await silent(page, seen, fetched, measured)
|
||
})
|
||
}
|
||
})
|
||
})
|