/** * Tests for the GPP CMP API (__gpp() global function). */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { installGppApi, removeGppApi, updateGppConsent, setGppDisplayStatus, setGppSignalStatus, getGppString, isGppApiInstalled, type GppPingReturn, type GppData, type GppEventData, type GppApiCallback, } from '../gpp-api'; import { type GppString, createDefaultSectionData, US_NATIONAL, US_CALIFORNIA, SECTION_REGISTRY, } from '../gpp'; // Section IDs (US_NATIONAL and US_CALIFORNIA are SectionDef objects, not numbers) const US_NATIONAL_ID = US_NATIONAL.id; // 7 const US_CALIFORNIA_ID = US_CALIFORNIA.id; // 8 // ── Helpers ─────────────────────────────────────────────────────────── /** Call __gpp and return the callback args as a promise. */ function callGpp( command: string, parameter?: unknown, ): Promise<{ data: unknown; success: boolean }> { return new Promise((resolve) => { window.__gpp!(command, (data: unknown, success: boolean) => { resolve({ data, success }); }, parameter); }); } /** Build a minimal GppString for testing. */ function buildTestGpp(sectionIds: number[] = [US_NATIONAL_ID]): GppString { const sections = new Map }>(); for (const id of sectionIds) { const def = SECTION_REGISTRY.get(id); if (def) { const data = createDefaultSectionData(def); sections.set(id, { data }); } } return { header: { version: 1, sectionIds, applicableSections: sectionIds, }, sections, }; } // ── Tests ───────────────────────────────────────────────────────────── describe('GPP CMP API', () => { beforeEach(() => { removeGppApi(); }); afterEach(() => { removeGppApi(); }); // ── Installation ────────────────────────────────────────────────── describe('installGppApi / removeGppApi', () => { it('installs __gpp on window', () => { expect(window.__gpp).toBeUndefined(); installGppApi(42, ['usnat']); expect(typeof window.__gpp).toBe('function'); expect(isGppApiInstalled()).toBe(true); }); it('removes __gpp from window', () => { installGppApi(42); removeGppApi(); expect(window.__gpp).toBeUndefined(); expect(isGppApiInstalled()).toBe(false); }); it('clears __gppQueue on remove', () => { (window as { __gppQueue?: unknown[] }).__gppQueue = [['ping', vi.fn()]]; installGppApi(42); removeGppApi(); expect((window as { __gppQueue?: unknown[] }).__gppQueue).toBeUndefined(); }); }); // ── ping command ────────────────────────────────────────────────── describe('ping command', () => { it('returns CMP metadata', async () => { installGppApi(42, ['usnat', 'usca']); const { data, success } = await callGpp('ping'); expect(success).toBe(true); const ping = data as GppPingReturn; expect(ping.gppVersion).toBe('1.1'); expect(ping.cmpStatus).toBe('loaded'); expect(ping.cmpDisplayStatus).toBe('hidden'); expect(ping.signalStatus).toBe('not ready'); expect(ping.supportedAPIs).toEqual(['usnat', 'usca']); expect(ping.cmpId).toBe(42); expect(ping.gppString).toBe(''); expect(ping.applicableSections).toEqual([]); }); it('reflects updated display status', async () => { installGppApi(1); setGppDisplayStatus('visible'); const { data } = await callGpp('ping'); expect((data as GppPingReturn).cmpDisplayStatus).toBe('visible'); }); it('reflects updated signal status', async () => { installGppApi(1); setGppSignalStatus('ready'); const { data } = await callGpp('ping'); expect((data as GppPingReturn).signalStatus).toBe('ready'); }); it('includes GPP string after consent update', async () => { installGppApi(1, ['usnat']); const gpp = buildTestGpp(); const encoded = updateGppConsent(gpp); expect(encoded).toBeTruthy(); const { data } = await callGpp('ping'); expect((data as GppPingReturn).gppString).toBe(encoded); expect((data as GppPingReturn).applicableSections).toEqual([US_NATIONAL_ID]); }); }); // ── getGPPData command ──────────────────────────────────────────── describe('getGPPData command', () => { it('returns empty data before consent', async () => { installGppApi(1); const { data, success } = await callGpp('getGPPData'); expect(success).toBe(true); const gppData = data as GppData; expect(gppData.gppString).toBe(''); expect(gppData.applicableSections).toEqual([]); expect(gppData.parsedSections).toEqual({}); }); it('returns populated data after consent update', async () => { installGppApi(1, ['usnat']); const gpp = buildTestGpp(); updateGppConsent(gpp); const { data } = await callGpp('getGPPData'); const gppData = data as GppData; expect(gppData.gppString).toBeTruthy(); expect(gppData.applicableSections).toEqual([US_NATIONAL_ID]); expect(gppData.parsedSections[US_NATIONAL_ID]).toBeDefined(); }); }); // ── getSection command ──────────────────────────────────────────── describe('getSection command', () => { it('returns null for missing prefix', async () => { installGppApi(1); const { data, success } = await callGpp('getSection', 'usnat'); expect(success).toBe(false); expect(data).toBeNull(); }); it('returns null when no parameter given', async () => { installGppApi(1); const { data, success } = await callGpp('getSection'); expect(success).toBe(false); expect(data).toBeNull(); }); it('returns section data by API prefix', async () => { installGppApi(1, ['usnat']); const gpp = buildTestGpp(); updateGppConsent(gpp); const { data, success } = await callGpp('getSection', 'usnat'); expect(success).toBe(true); expect(data).toBeDefined(); expect((data as Record).Version).toBeDefined(); }); it('returns null for unregistered prefix', async () => { installGppApi(1); updateGppConsent(buildTestGpp()); const { data, success } = await callGpp('getSection', 'nonexistent'); expect(success).toBe(false); expect(data).toBeNull(); }); }); // ── hasSection command ──────────────────────────────────────────── describe('hasSection command', () => { it('returns false when section not present', async () => { installGppApi(1); const { data, success } = await callGpp('hasSection', 'usnat'); expect(success).toBe(true); expect(data).toBe(false); }); it('returns true when section present', async () => { installGppApi(1, ['usnat']); updateGppConsent(buildTestGpp()); const { data, success } = await callGpp('hasSection', 'usnat'); expect(success).toBe(true); expect(data).toBe(true); }); it('returns false for empty parameter', async () => { installGppApi(1); const { data, success } = await callGpp('hasSection'); expect(success).toBe(true); expect(data).toBe(false); }); }); // ── addEventListener / removeEventListener ──────────────────────── describe('addEventListener', () => { it('registers a listener and fires immediately', async () => { installGppApi(1, ['usnat']); const { data, success } = await callGpp('addEventListener'); expect(success).toBe(true); const event = data as GppEventData; expect(event.eventName).toBe('listenerRegistered'); expect(event.listenerId).toBe(1); expect(event.pingData.cmpStatus).toBe('loaded'); }); it('assigns unique listener IDs', async () => { installGppApi(1); const { data: d1 } = await callGpp('addEventListener'); const { data: d2 } = await callGpp('addEventListener'); expect((d1 as GppEventData).listenerId).toBe(1); expect((d2 as GppEventData).listenerId).toBe(2); }); it('notifies listeners on consent update', () => { installGppApi(1, ['usnat']); const events: GppEventData[] = []; window.__gpp!('addEventListener', (data: unknown) => { events.push(data as GppEventData); }); // First event is listenerRegistered expect(events).toHaveLength(1); expect(events[0].eventName).toBe('listenerRegistered'); // Update consent — should fire signalStatus event updateGppConsent(buildTestGpp()); expect(events).toHaveLength(2); expect(events[1].eventName).toBe('signalStatus'); expect(events[1].data).toBeDefined(); expect((events[1].data as GppData).gppString).toBeTruthy(); }); it('swallows errors thrown by listeners', () => { installGppApi(1); const throwingCallback: GppApiCallback = () => { throw new Error('Listener error'); }; // Should not throw expect(() => { window.__gpp!('addEventListener', throwingCallback); }).not.toThrow(); // Should not throw during consent update either expect(() => { updateGppConsent(buildTestGpp()); }).not.toThrow(); }); }); describe('removeEventListener', () => { it('removes an existing listener', async () => { installGppApi(1); // Add listener const { data } = await callGpp('addEventListener'); const listenerId = (data as GppEventData).listenerId; // Remove it const { data: removed, success } = await callGpp('removeEventListener', listenerId); expect(success).toBe(true); expect(removed).toBe(true); }); it('returns false for non-existent listener', async () => { installGppApi(1); const { data, success } = await callGpp('removeEventListener', 999); expect(success).toBe(false); expect(data).toBe(false); }); it('stops notifications after removal', () => { installGppApi(1, ['usnat']); const events: GppEventData[] = []; let listenerId: number; window.__gpp!('addEventListener', (data: unknown) => { const event = data as GppEventData; events.push(event); listenerId = event.listenerId; }); // Remove the listener window.__gpp!('removeEventListener', (_d: unknown, _s: boolean) => {}, listenerId!); // Update consent — should NOT fire to removed listener const eventsBefore = events.length; updateGppConsent(buildTestGpp()); expect(events.length).toBe(eventsBefore); }); }); // ── Unknown commands ────────────────────────────────────────────── describe('unknown commands', () => { it('returns false for unknown commands', async () => { installGppApi(1); const { data, success } = await callGpp('unknownCommand'); expect(success).toBe(false); expect(data).toBe(false); }); }); // ── Uninitialised state ─────────────────────────────────────────── describe('uninitialised state', () => { it('returns false when API not installed', () => { // Manually set __gpp to the handler without initialising state // by calling removeGppApi first to clear state, then manually assigning removeGppApi(); // Directly import and call the handler — simulate calling before install let callbackData: unknown; let callbackSuccess: boolean | undefined; // We can't call __gpp because it's removed, but we can test via getGppString expect(getGppString()).toBe(''); expect(isGppApiInstalled()).toBe(false); }); }); // ── updateGppConsent ────────────────────────────────────────────── describe('updateGppConsent', () => { it('returns the encoded GPP string', () => { installGppApi(1, ['usnat']); const gpp = buildTestGpp(); const result = updateGppConsent(gpp); expect(result).toBeTruthy(); expect(typeof result).toBe('string'); // GPP strings start with 'D' (header prefix) expect(result.startsWith('D')).toBe(true); }); it('updates the gppString accessible via getGppString', () => { installGppApi(1); expect(getGppString()).toBe(''); updateGppConsent(buildTestGpp()); expect(getGppString()).toBeTruthy(); }); it('sets signalStatus to ready', async () => { installGppApi(1); updateGppConsent(buildTestGpp()); const { data } = await callGpp('ping'); expect((data as GppPingReturn).signalStatus).toBe('ready'); }); it('works with multiple sections', () => { installGppApi(1, ['usnat', 'usca']); const gpp = buildTestGpp([US_NATIONAL_ID, US_CALIFORNIA_ID]); const result = updateGppConsent(gpp); expect(result).toBeTruthy(); // Should contain ~ separator for sections expect(result).toContain('~'); }); }); // ── Queue processing ────────────────────────────────────────────── describe('queue processing', () => { it('processes queued calls on install', () => { // Set up a queue before installing const results: Array<{ data: unknown; success: boolean }> = []; const cb = (data: unknown, success: boolean) => { results.push({ data, success }); }; (window as { __gppQueue?: unknown[] }).__gppQueue = [ ['ping', cb], ]; installGppApi(1, ['usnat']); // The queued ping should have been processed expect(results).toHaveLength(1); expect(results[0].success).toBe(true); expect((results[0].data as GppPingReturn).cmpStatus).toBe('loaded'); }); it('clears the queue after processing', () => { (window as { __gppQueue?: unknown[] }).__gppQueue = [ ['ping', vi.fn()], ]; installGppApi(1); expect((window as { __gppQueue?: unknown[] }).__gppQueue).toEqual([]); }); }); // ── Coexistence with __tcfapi ───────────────────────────────────── describe('coexistence', () => { it('does not interfere with other window globals', () => { // Set a mock __tcfapi const mockTcf = vi.fn(); (window as { __tcfapi?: unknown }).__tcfapi = mockTcf; installGppApi(1); // __tcfapi should still be intact expect((window as { __tcfapi?: unknown }).__tcfapi).toBe(mockTcf); removeGppApi(); // __tcfapi should still be intact after removal expect((window as { __tcfapi?: unknown }).__tcfapi).toBe(mockTcf); // Clean up delete (window as { __tcfapi?: unknown }).__tcfapi; }); }); // ── setGppDisplayStatus / setGppSignalStatus ────────────────────── describe('status setters', () => { it('setGppDisplayStatus updates display status', async () => { installGppApi(1); setGppDisplayStatus('visible'); const { data } = await callGpp('ping'); expect((data as GppPingReturn).cmpDisplayStatus).toBe('visible'); setGppDisplayStatus('disabled'); const { data: d2 } = await callGpp('ping'); expect((d2 as GppPingReturn).cmpDisplayStatus).toBe('disabled'); }); it('setGppSignalStatus updates signal status', async () => { installGppApi(1); setGppSignalStatus('ready'); const { data } = await callGpp('ping'); expect((data as GppPingReturn).signalStatus).toBe('ready'); }); it('no-ops when API not installed', () => { // Should not throw expect(() => setGppDisplayStatus('visible')).not.toThrow(); expect(() => setGppSignalStatus('ready')).not.toThrow(); }); }); });