import React from 'react' import { cleanup, render, waitFor } from '@testing-library/react' function prepareEnvironment() { document.head.innerHTML = '' const storage = new Map([['academy.analytics.visitor-id', 'visitor-123']]) vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => storage.get(String(key)) ?? null) vi.spyOn(Storage.prototype, 'setItem').mockImplementation((key, value) => { storage.set(String(key), String(value)) }) globalThis.fetch = vi.fn(() => Promise.resolve({ ok: true, headers: { get: () => 'application/json' }, json: () => Promise.resolve({ ok: true }) })) } function cleanupEnvironment() { cleanup() vi.restoreAllMocks() document.head.innerHTML = '' } test('academy search click attribution uses sendBeacon without blocking navigation', async () => { prepareEnvironment() const { trackAcademySearchResultClick } = await import('./academyAnalytics.js') Object.defineProperty(navigator, 'sendBeacon', { configurable: true, value: vi.fn(() => true), }) const result = trackAcademySearchResultClick({ eventUrl: '/academy/analytics/events', pageName: 'academy_prompts_index', }, { query: 'robot mascot', resultsCount: 12, filters: { difficulty: 'beginner' }, }, { contentType: 'academy_prompt', contentId: 123, position: 3, }) expect(result).toBeUndefined() expect(navigator.sendBeacon).toHaveBeenCalledTimes(1) expect(globalThis.fetch).not.toHaveBeenCalled() cleanupEnvironment() }) test('academy search click attribution falls back to keepalive fetch when sendBeacon cannot queue', async () => { prepareEnvironment() const { trackAcademySearchResultClick } = await import('./academyAnalytics.js') Object.defineProperty(navigator, 'sendBeacon', { configurable: true, value: vi.fn(() => false), }) trackAcademySearchResultClick({ eventUrl: '/academy/analytics/events', pageName: 'academy_prompts_index', }, { query: 'robot mascot', resultsCount: 12, filters: { difficulty: 'beginner' }, }, { contentType: 'academy_prompt', contentId: 123, position: 3, }) expect(globalThis.fetch).toHaveBeenCalledTimes(1) expect(globalThis.fetch.mock.calls[0][1].keepalive).toBe(true) cleanupEnvironment() }) test('academy page analytics includes custom metadata and varies page-view once keys by tracking context', async () => { prepareEnvironment() const { useAcademyPageAnalytics } = await import('./academyAnalytics.js') Object.defineProperty(navigator, 'sendBeacon', { configurable: true, value: undefined, }) function TestPage({ analytics }) { useAcademyPageAnalytics(analytics) return React.createElement('div', null, 'Academy page') } const { rerender } = render( React.createElement(TestPage, { analytics: { enabled: true, eventUrl: '/academy/analytics/events', pageName: 'academy_prompts_popular', contentType: 'academy_prompt_popular', contentId: null, trackingKey: 'period:30d', metadata: { period: '30d', period_days: 30 }, }, }), ) await waitFor(() => { expect(globalThis.fetch).toHaveBeenCalledTimes(2) }) const firstPageView = globalThis.fetch.mock.calls .map((call) => JSON.parse(call[1].body)) .find((payload) => payload.event_type === 'academy_page_view') expect(firstPageView.metadata.period).toBe('30d') expect(firstPageView.metadata.period_days).toBe(30) rerender( React.createElement(TestPage, { analytics: { enabled: true, eventUrl: '/academy/analytics/events', pageName: 'academy_prompts_popular', contentType: 'academy_prompt_popular', contentId: null, trackingKey: 'period:7d', metadata: { period: '7d', period_days: 7 }, }, }), ) await waitFor(() => { expect(globalThis.fetch).toHaveBeenCalledTimes(4) }) const pageViews = globalThis.fetch.mock.calls .map((call) => JSON.parse(call[1].body)) .filter((payload) => payload.event_type === 'academy_page_view') expect(pageViews).toHaveLength(2) expect(pageViews[1].metadata.period).toBe('7d') cleanupEnvironment() })