/** * Tests for AdvancedAnalytics feature */ import { AdvancedAnalytics } from '../AdvancedAnalytics'; import { ConversionIQConfig } from '../../types/index'; describe('AdvancedAnalytics', () => { let advancedAnalytics: AdvancedAnalytics; let mockConfig: ConversionIQConfig; let mockSdk: any; beforeEach(() => { mockConfig = { apiKey: 'test-key', endpoint: 'https://api.test.com', debug: false }; mockSdk = { track: jest.fn() }; // Mock localStorage Object.defineProperty(global, 'localStorage', { value: { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn() }, writable: true, configurable: true }); // Mock MutationObserver global.MutationObserver = jest.fn().mockImplementation((callback) => ({ observe: jest.fn(), disconnect: jest.fn(), takeRecords: jest.fn() })); jest.spyOn(document, 'addEventListener').mockImplementation(() => {}); jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); }); afterEach(() => { jest.restoreAllMocks(); jest.clearAllMocks(); }); describe('initialization', () => { it('should initialize session data', () => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); const sessionData = advancedAnalytics.getSessionData(); expect(sessionData.id).toContain('ciq_session_'); expect(sessionData.startTime).toBeGreaterThan(0); expect(sessionData.lastActivity).toBeGreaterThan(0); expect(sessionData.pageViews).toBe(0); expect(sessionData.events).toBe(0); expect(sessionData.duration).toBe(0); }); it('should setup event listeners for session tracking', () => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); expect(document.addEventListener).toHaveBeenCalledWith('visibilitychange', expect.any(Function)); expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function), expect.any(Object)); expect(document.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), expect.any(Object)); expect(document.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function), expect.any(Object)); expect(document.addEventListener).toHaveBeenCalledWith('mousemove', expect.any(Function), expect.any(Object)); expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function)); }); }); describe('trackPageView', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should track page view with default URL', () => { advancedAnalytics.trackPageView(); expect(mockSdk.track).toHaveBeenCalledWith('enhanced_page_view', { url: window.location.href, title: document.title, referrer: document.referrer, timestamp: expect.any(Number), viewport: { width: window.innerWidth, height: window.innerHeight }, screen: { width: screen.width, height: screen.height }, sessionId: expect.stringContaining('ciq_session_'), pageNumber: 1 }); }); it('should track page view with custom URL', () => { advancedAnalytics.trackPageView('https://example.com/custom'); expect(mockSdk.track).toHaveBeenCalledWith('enhanced_page_view', expect.objectContaining({ url: 'https://example.com/custom', pageNumber: 1 })); }); it('should increment page view count', () => { advancedAnalytics.trackPageView(); advancedAnalytics.trackPageView(); const sessionData = advancedAnalytics.getSessionData(); expect(sessionData.pageViews).toBe(2); }); it('should add page view to user journey', () => { advancedAnalytics.trackPageView(); const journey = advancedAnalytics.getUserJourney(); expect(journey.length).toBe(1); expect(journey[0].event).toBe('page_view'); }); }); describe('trackEngagement', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); // Mock scroll properties Object.defineProperty(document.documentElement, 'scrollHeight', { configurable: true, value: 2000 }); Object.defineProperty(window, 'innerHeight', { configurable: true, value: 800 }); Object.defineProperty(window, 'pageYOffset', { configurable: true, value: 0 }); }); it('should track user engagement', () => { advancedAnalytics.trackEngagement(); expect(mockSdk.track).toHaveBeenCalledWith('user_engagement', { timeOnPage: expect.any(Number), scrollDepth: expect.any(Number), clickCount: 0, sessionDuration: expect.any(Number), pageViews: 0, sessionId: expect.stringContaining('ciq_session_') }); }); }); describe('setupFunnelAnalysis', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should setup funnel analysis with selectors', () => { const steps = [ { name: 'Step 1', selector: '.step-1' }, { name: 'Step 2', selector: '.step-2' }, { name: 'Step 3', selector: '.step-3' } ]; advancedAnalytics.setupFunnelAnalysis(steps); expect(global.MutationObserver).toHaveBeenCalled(); }); it('should setup funnel analysis with URLs', () => { const steps = [ { name: 'Landing', url: '/landing' }, { name: 'Signup', url: '/signup' }, { name: 'Complete', url: '/complete' } ]; advancedAnalytics.setupFunnelAnalysis(steps); // Should not crash expect(true).toBe(true); }); }); describe('trackCohort', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should track cohort with user attributes', () => { const userAttributes = { plan: 'premium', signupDate: '2024-01-01' }; advancedAnalytics.trackCohort('Premium Users', userAttributes); expect(mockSdk.track).toHaveBeenCalledWith('cohort_analysis', { cohortName: 'Premium Users', userAttributes, sessionId: expect.stringContaining('ciq_session_'), firstVisit: expect.any(Boolean), timestamp: expect.any(Number) }); }); it('should track cohort without user attributes', () => { advancedAnalytics.trackCohort('All Users'); expect(mockSdk.track).toHaveBeenCalledWith('cohort_analysis', { cohortName: 'All Users', userAttributes: {}, sessionId: expect.stringContaining('ciq_session_'), firstVisit: expect.any(Boolean), timestamp: expect.any(Number) }); }); it('should detect first visit', () => { (localStorage.getItem as jest.Mock).mockReturnValue(null); advancedAnalytics.trackCohort('New Users'); expect(localStorage.setItem).toHaveBeenCalledWith('conversioniq_visited', 'true'); expect(mockSdk.track).toHaveBeenCalledWith('cohort_analysis', expect.objectContaining({ firstVisit: true })); }); it('should detect returning visit', () => { (localStorage.getItem as jest.Mock).mockReturnValue('true'); advancedAnalytics.trackCohort('Returning Users'); expect(mockSdk.track).toHaveBeenCalledWith('cohort_analysis', expect.objectContaining({ firstVisit: false })); }); it('should handle localStorage errors', () => { (localStorage.getItem as jest.Mock).mockImplementation(() => { throw new Error('localStorage not available'); }); advancedAnalytics.trackCohort('Users'); expect(mockSdk.track).toHaveBeenCalledWith('cohort_analysis', expect.objectContaining({ firstVisit: false })); }); }); describe('trackAttribution', () => { beforeEach(() => { // Reset window.location to default delete (window as any).location; window.location = { ...window.location, search: '' } as any; advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should track attribution from Google', () => { Object.defineProperty(document, 'referrer', { value: 'https://www.google.com/search', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', { source: 'google', medium: 'organic', campaign: null, referrer: 'https://www.google.com/search', landingPage: window.location.href, sessionId: expect.stringContaining('ciq_session_'), timestamp: expect.any(Number) }); }); it('should track attribution from Facebook', () => { Object.defineProperty(document, 'referrer', { value: 'https://www.facebook.com/post', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'facebook', medium: 'social' })); }); it('should track direct traffic', () => { Object.defineProperty(document, 'referrer', { value: '', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'direct', medium: 'direct' })); }); it('should track referral traffic', () => { Object.defineProperty(document, 'referrer', { value: 'https://example.com/page', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'referral', medium: 'referral' })); }); it('should extract UTM campaign', () => { Object.defineProperty(window, 'location', { value: { ...window.location, search: '?utm_campaign=summer-sale&utm_medium=email' }, configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ campaign: 'summer-sale', medium: 'email' })); }); }); describe('trackConversionFunnel', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should track conversion funnel step', () => { advancedAnalytics.trackConversionFunnel('Checkout', 'Payment Info', 99.99); expect(mockSdk.track).toHaveBeenCalledWith('conversion_funnel', { funnelName: 'Checkout', stepName: 'Payment Info', value: 99.99, sessionId: expect.stringContaining('ciq_session_'), userJourney: expect.any(Array), timestamp: expect.any(Number) }); }); it('should track conversion funnel step without value', () => { advancedAnalytics.trackConversionFunnel('Signup', 'Email'); expect(mockSdk.track).toHaveBeenCalledWith('conversion_funnel', expect.objectContaining({ funnelName: 'Signup', stepName: 'Email', value: 0 })); }); }); describe('trackUserSegment', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should track user segment with properties', () => { const properties = { industry: 'technology', companySize: '50-200' }; advancedAnalytics.trackUserSegment('Enterprise', properties); expect(mockSdk.track).toHaveBeenCalledWith('user_segment', { segment: 'Enterprise', properties, sessionId: expect.stringContaining('ciq_session_'), sessionData: expect.any(Object), timestamp: expect.any(Number) }); }); it('should track user segment without properties', () => { advancedAnalytics.trackUserSegment('Free Users'); expect(mockSdk.track).toHaveBeenCalledWith('user_segment', expect.objectContaining({ segment: 'Free Users', properties: {} })); }); }); describe('user journey', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should add custom events to user journey', () => { advancedAnalytics.addToUserJourney('button_click', { button: 'signup' }); const journey = advancedAnalytics.getUserJourney(); expect(journey.length).toBe(1); expect(journey[0].event).toBe('button_click'); expect(journey[0].data).toEqual({ button: 'signup' }); }); it('should increment events count', () => { advancedAnalytics.addToUserJourney('event1', {}); advancedAnalytics.addToUserJourney('event2', {}); const sessionData = advancedAnalytics.getSessionData(); expect(sessionData.events).toBe(2); }); it('should limit journey to 50 events', () => { for (let i = 0; i < 60; i++) { advancedAnalytics.addToUserJourney(`event${i}`, { index: i }); } const journey = advancedAnalytics.getUserJourney(); expect(journey.length).toBe(50); expect(journey[0].data.index).toBe(10); // First 10 should be removed }); it('should return copy of user journey', () => { advancedAnalytics.addToUserJourney('test', {}); const journey1 = advancedAnalytics.getUserJourney(); const journey2 = advancedAnalytics.getUserJourney(); expect(journey1).toEqual(journey2); expect(journey1).not.toBe(journey2); // Different objects }); }); describe('session data', () => { beforeEach(() => { advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should return copy of session data', () => { const sessionData1 = advancedAnalytics.getSessionData(); const sessionData2 = advancedAnalytics.getSessionData(); expect(sessionData1).toEqual(sessionData2); expect(sessionData1).not.toBe(sessionData2); // Different objects }); it('should update session duration', () => { jest.useFakeTimers(); const initialData = advancedAnalytics.getSessionData(); expect(initialData.duration).toBe(0); jest.advanceTimersByTime(5000); advancedAnalytics.trackPageView(); const updatedData = advancedAnalytics.getSessionData(); expect(updatedData.duration).toBeGreaterThan(0); jest.useRealTimers(); }); }); describe('traffic source detection', () => { beforeEach(() => { // Reset window.location to default delete (window as any).location; window.location = { ...window.location, search: '' } as any; advancedAnalytics = new AdvancedAnalytics(mockConfig, mockSdk); }); it('should detect Bing as source', () => { Object.defineProperty(document, 'referrer', { value: 'https://www.bing.com/search', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'bing', medium: 'organic' })); }); it('should detect Yahoo as source', () => { Object.defineProperty(document, 'referrer', { value: 'https://search.yahoo.com', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'yahoo', medium: 'organic' })); }); it('should detect Twitter as source', () => { Object.defineProperty(document, 'referrer', { value: 'https://twitter.com/post', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'twitter', medium: 'social' })); }); it('should detect LinkedIn as source', () => { Object.defineProperty(document, 'referrer', { value: 'https://www.linkedin.com/feed', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'linkedin', medium: 'social' })); }); it('should detect Instagram as source', () => { Object.defineProperty(document, 'referrer', { value: 'https://www.instagram.com/post', configurable: true }); advancedAnalytics.trackAttribution(); expect(mockSdk.track).toHaveBeenCalledWith('attribution', expect.objectContaining({ source: 'instagram', medium: 'social' })); }); }); describe('session ID generation', () => { it('should generate unique session IDs', () => { const analytics1 = new AdvancedAnalytics(mockConfig, mockSdk); const analytics2 = new AdvancedAnalytics(mockConfig, mockSdk); const session1 = analytics1.getSessionData(); const session2 = analytics2.getSessionData(); expect(session1.id).not.toBe(session2.id); expect(session1.id).toMatch(/^ciq_session_\d+_[a-z0-9]+$/); expect(session2.id).toMatch(/^ciq_session_\d+_[a-z0-9]+$/); }); }); });