/** * Tests for the loader ↔ bundle blocker bridge. * * ``consent-loader.js`` and ``consent-bundle.js`` are compiled as * separate rollup IIFEs, so each one inlines its own copy of * ``blocker.ts`` with private module state. The bundle therefore * can't reach the loader's ``acceptedCategories`` set via a direct * import — it has to call through ``window.__consentos._updateBlocker``, * which the loader sets after ``installBlocker()``. * * We mock the imports the banner module pulls in so importing * ``banner.ts`` here doesn't try to hit real network / timers. */ import { beforeEach, describe, expect, it, vi } from 'vitest'; // Seed ``window.__consentos`` before banner.ts's init() IIFE runs at // import time. Without this, destructuring ``window.__consentos`` at // the top of init() throws and fills the test output with noise. vi.hoisted(() => { (globalThis as any).window = (globalThis as any).window || globalThis; (globalThis as any).window.__consentos = { siteId: '', apiBase: '', cdnBase: '', loaded: false, }; }); vi.mock('../consent', () => ({ buildConsentState: vi.fn(() => ({ accepted: [], rejected: [] })), readConsent: vi.fn(() => null), writeConsent: vi.fn(), })); vi.mock('../gcm', () => ({ buildGcmStateFromCategories: vi.fn(() => ({})), updateGcm: vi.fn(), })); vi.mock('../i18n', () => ({ DEFAULT_TRANSLATIONS: {}, detectLocale: vi.fn(() => 'en'), interpolate: vi.fn((s: string) => s), loadTranslations: vi.fn(async () => ({})), renderLinks: vi.fn((s: string) => s), })); vi.mock('../a11y', () => ({ announce: vi.fn(), createLiveRegion: vi.fn(), focusFirst: vi.fn(), onEscape: vi.fn(() => () => {}), prefersReducedMotion: vi.fn(() => false), trapFocus: vi.fn(() => () => {}), })); // Prevent banner.ts's init() IIFE from running against real globals. vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('mocked')))); import { updateAcceptedCategories } from '../banner'; describe('loader ↔ bundle blocker bridge', () => { beforeEach(() => { window.__consentos = { siteId: 'test', apiBase: 'https://api.example.com', cdnBase: 'https://cdn.example.com', loaded: false, }; }); it('calls window.__consentos._updateBlocker when the bridge is present', () => { const bridge = vi.fn(); window.__consentos._updateBlocker = bridge; updateAcceptedCategories(['necessary', 'analytics']); expect(bridge).toHaveBeenCalledTimes(1); expect(bridge).toHaveBeenCalledWith(['necessary', 'analytics']); }); it('forwards the exact array reference so the loader sees every slug', () => { const bridge = vi.fn(); window.__consentos._updateBlocker = bridge; const accepted = ['necessary', 'functional', 'marketing'] as const; updateAcceptedCategories([...accepted]); const args = bridge.mock.calls[0][0]; expect(args).toEqual(['necessary', 'functional', 'marketing']); }); it('warns and returns cleanly when the bridge is missing', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); delete window.__consentos._updateBlocker; expect(() => updateAcceptedCategories(['necessary'])).not.toThrow(); expect(warn).toHaveBeenCalledWith( expect.stringContaining('blocker bridge missing'), ); warn.mockRestore(); }); it('warns when window.__consentos is missing entirely', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); // @ts-expect-error — simulating a pre-init state window.__consentos = undefined; expect(() => updateAcceptedCategories(['necessary'])).not.toThrow(); expect(warn).toHaveBeenCalled(); warn.mockRestore(); }); });