UNPKG

@tinytapanalytics/sdk

Version:

Behavioral psychology platform that detects visitor frustration, predicts abandonment, and helps you save at-risk conversions in real-time

1,203 lines (957 loc) 34.7 kB
/** * Tests for main TinyTapAnalytics SDK */ import TinyTapAnalyticsSDK from '../index'; import { EventQueue } from '../core/EventQueue'; import { NetworkManager } from '../core/NetworkManager'; import { PrivacyManager } from '../core/PrivacyManager'; import { EnvironmentDetector } from '../core/EnvironmentDetector'; import { ErrorHandler } from '../core/ErrorHandler'; import { MicroInteractionTracking } from '../features/MicroInteractionTracking'; import type { TinyTapAnalyticsConfig } from '../types/index'; // Mock all dependencies jest.mock('../core/EventQueue'); jest.mock('../core/NetworkManager'); jest.mock('../core/PrivacyManager'); jest.mock('../core/EnvironmentDetector'); jest.mock('../core/ErrorHandler'); jest.mock('../features/MicroInteractionTracking'); describe('TinyTapAnalyticsSDK', () => { let sdk: TinyTapAnalyticsSDK; let mockConfig: TinyTapAnalyticsConfig; let mockEventQueue: jest.Mocked<EventQueue>; let mockNetworkManager: jest.Mocked<NetworkManager>; let mockPrivacyManager: jest.Mocked<PrivacyManager>; let mockEnvironmentDetector: jest.Mocked<EnvironmentDetector>; let mockErrorHandler: jest.Mocked<ErrorHandler>; beforeEach(() => { // Clear all mocks jest.clearAllMocks(); // Setup DOM document.body.innerHTML = '<div id="test"></div>'; Object.defineProperty(document, 'readyState', { value: 'complete', writable: true, configurable: true }); // Mock window properties Object.defineProperty(window, 'location', { value: { href: 'https://test.com/page', pathname: '/page', search: '?test=1', hash: '#section' }, writable: true, configurable: true }); Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }); Object.defineProperty(window, 'innerHeight', { value: 768, writable: true }); Object.defineProperty(screen, 'width', { value: 1920, writable: true }); Object.defineProperty(screen, 'height', { value: 1080, writable: true }); Object.defineProperty(document, 'title', { value: 'Test Page', writable: true, configurable: true }); Object.defineProperty(document, 'referrer', { value: 'https://referrer.com', writable: true, configurable: true }); // Mock navigator.userAgent Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Test Browser)', writable: true, configurable: true }); // Create mock config mockConfig = { websiteId: 'test-website-123', apiKey: 'test-api-key', endpoint: 'https://api.test.com', debug: false }; // Setup mock implementations mockEventQueue = { enqueue: jest.fn(async (event: any) => { return Promise.resolve(); }), flush: jest.fn().mockResolvedValue(undefined), destroy: jest.fn() } as any; mockNetworkManager = { send: jest.fn().mockResolvedValue({ success: true }) } as any; mockPrivacyManager = { init: jest.fn().mockResolvedValue(undefined), canTrack: jest.fn((type: string) => true), updateConsent: jest.fn(), getConsentStatus: jest.fn().mockReturnValue({ essential: true, functional: true, analytics: true, marketing: false }), destroy: jest.fn() } as any; mockEnvironmentDetector = { isSPA: jest.fn().mockReturnValue(false), getFramework: jest.fn().mockReturnValue('unknown') } as any; mockErrorHandler = { handle: jest.fn(), destroy: jest.fn() } as any; // Mock constructors - return the same mock instance every time (EventQueue as jest.MockedClass<typeof EventQueue>).mockImplementation(() => mockEventQueue); (NetworkManager as jest.MockedClass<typeof NetworkManager>).mockImplementation(() => mockNetworkManager); (PrivacyManager as jest.MockedClass<typeof PrivacyManager>).mockImplementation(() => mockPrivacyManager); (EnvironmentDetector as jest.MockedClass<typeof EnvironmentDetector>).mockImplementation(() => mockEnvironmentDetector); (ErrorHandler as jest.MockedClass<typeof ErrorHandler>).mockImplementation(() => mockErrorHandler); // Mock console methods jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); // Create SDK instance sdk = new TinyTapAnalyticsSDK(mockConfig); }); afterEach(() => { jest.restoreAllMocks(); }); describe('Constructor', () => { it('should initialize with provided config', () => { expect(sdk).toBeInstanceOf(TinyTapAnalyticsSDK); expect(ErrorHandler).toHaveBeenCalledWith(expect.objectContaining({ websiteId: 'test-website-123', endpoint: 'https://api.test.com' })); }); it('should apply default config values', () => { const sdkWithDefaults = new TinyTapAnalyticsSDK({ websiteId: 'test' }); expect(NetworkManager).toHaveBeenCalledWith( expect.objectContaining({ batchSize: 10, flushInterval: 5000, timeout: 5000, enableAutoTracking: true, enablePrivacyMode: true }), expect.any(Object) ); }); it('should generate a session ID', () => { // Session ID should be unique each time const sdk1 = new TinyTapAnalyticsSDK(mockConfig); const sdk2 = new TinyTapAnalyticsSDK(mockConfig); expect(sdk1).not.toBe(sdk2); }); }); describe('init()', () => { it('should initialize successfully', async () => { await sdk.init(); expect(mockPrivacyManager.init).toHaveBeenCalled(); expect(mockPrivacyManager.canTrack).toHaveBeenCalledWith('essential'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'page_view', website_id: 'test-website-123' }) ); }); it('should not initialize twice', async () => { await sdk.init(); await sdk.init(); expect(console.warn).toHaveBeenCalledWith('TinyTapAnalytics: Already initialized'); }); it('should not track if privacy settings prevent it', async () => { mockPrivacyManager.canTrack.mockReturnValue(false); await sdk.init(); expect(mockEventQueue.enqueue).not.toHaveBeenCalled(); }); it('should wait for DOM if loading', async () => { Object.defineProperty(document, 'readyState', { value: 'loading', writable: true, configurable: true }); const initPromise = sdk.init(); // Simulate DOM ready setTimeout(() => { document.dispatchEvent(new Event('DOMContentLoaded')); }, 10); await initPromise; expect(mockPrivacyManager.init).toHaveBeenCalled(); }); it('should handle initialization errors', async () => { mockPrivacyManager.init.mockRejectedValue(new Error('Privacy init failed')); await sdk.init(); expect(mockErrorHandler.handle).toHaveBeenCalledWith( expect.any(Error), 'initialization' ); }); it('should log when debug is enabled', async () => { const debugSdk = new TinyTapAnalyticsSDK({ ...mockConfig, debug: true }); await debugSdk.init(); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: Initialized successfully', expect.objectContaining({ endpoint: 'https://api.test.com', website: 'test-website-123' }) ); }); }); describe('identify()', () => { it('should set user ID', async () => { await sdk.init(); jest.clearAllMocks(); sdk.identify('user-123'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'identify', user_id: 'user-123', metadata: expect.objectContaining({ user_id: 'user-123', context: {} }) }) ); }); it('should set user context', async () => { await sdk.init(); jest.clearAllMocks(); sdk.identify('user-123', { email: 'test@example.com', name: 'Test User' }); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ context: { email: 'test@example.com', name: 'Test User' } }) }) ); }); it('should log when debug is enabled', () => { const debugSdk = new TinyTapAnalyticsSDK({ ...mockConfig, debug: true }); debugSdk.identify('user-123', { email: 'test@example.com' }); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: User identified', { userId: 'user-123', context: { email: 'test@example.com' } } ); }); it('should handle errors', async () => { mockEventQueue.enqueue.mockRejectedValue(new Error('Network error')); sdk.identify('user-123'); // Wait for async track operation to complete await new Promise(resolve => setTimeout(resolve, 0)); expect(mockErrorHandler.handle).toHaveBeenCalled(); }); }); describe('track()', () => { it('should track custom events', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.track('button_click', { button_id: 'submit' }); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'button_click', website_id: 'test-website-123', metadata: expect.objectContaining({ button_id: 'submit' }) }) ); }); it('should include device information', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.track('test_event'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ device_type: expect.any(String), viewport_width: 1024, viewport_height: 768, screen_width: 1920, screen_height: 1080 }) }) ); }); it('should respect privacy settings', async () => { await sdk.init(); mockPrivacyManager.canTrack.mockReturnValue(false); jest.clearAllMocks(); await sdk.track('analytics_event'); expect(mockEventQueue.enqueue).not.toHaveBeenCalled(); }); it('should queue events before initialization', async () => { const uninitializedSdk = new TinyTapAnalyticsSDK(mockConfig); await uninitializedSdk.track('early_event', { data: 'test' }); // Event should not be tracked immediately expect(mockEventQueue.enqueue).not.toHaveBeenCalled(); }); it('should handle tracking errors', async () => { await sdk.init(); mockEventQueue.enqueue.mockRejectedValue(new Error('Queue full')); jest.clearAllMocks(); await sdk.track('test_event'); // Error handler called during init and track expect(mockErrorHandler.handle).toHaveBeenCalled(); }); }); describe('trackConversion()', () => { it('should track conversion with all data', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.trackConversion({ value: 99.99, currency: 'EUR', transactionId: 'txn-123', items: [{ id: 'item-1', name: 'Product', price: 99.99 }], metadata: { campaign: 'spring-sale' } }); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'conversion', metadata: expect.objectContaining({ value: 99.99, currency: 'EUR', transaction_id: 'txn-123', items: expect.arrayContaining([ expect.objectContaining({ id: 'item-1' }) ]) }) }) ); }); it('should use USD as default currency', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.trackConversion({ value: 49.99, transactionId: 'txn-456' }); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ currency: 'USD' }) }) ); }); }); describe('trackPageView()', () => { it('should track current page by default', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.trackPageView(); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'page_view', metadata: expect.objectContaining({ url: 'https://test.com/page', title: 'Test Page', referrer: 'https://referrer.com', path: '/page', search: '?test=1', hash: '#section' }) }) ); }); it('should track custom URL', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.trackPageView('https://custom.com/page'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ url: 'https://custom.com/page' }) }) ); }); }); describe('trackClick()', () => { it('should track element click with selector', async () => { await sdk.init(); jest.clearAllMocks(); const button = document.createElement('button'); button.id = 'submit-btn'; button.textContent = 'Submit'; button.setAttribute('data-track', 'true'); document.body.appendChild(button); await sdk.trackClick('#submit-btn'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click', metadata: expect.objectContaining({ element: '#submit-btn', element_type: 'button', element_text: 'Submit' }) }) ); button.remove(); }); it('should track element click with Element object', async () => { await sdk.init(); jest.clearAllMocks(); const link = document.createElement('a'); link.className = 'cta-link'; link.textContent = 'Click me'; document.body.appendChild(link); await sdk.trackClick(link, { campaign: 'header' }); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ element: '.cta-link', element_text: 'Click me', metadata: { campaign: 'header' } }) }) ); link.remove(); }); it('should warn if element not found', async () => { const debugSdk = new TinyTapAnalyticsSDK({ ...mockConfig, debug: true }); await debugSdk.init(); await debugSdk.trackClick('#non-existent'); expect(console.warn).toHaveBeenCalledWith( 'TinyTapAnalytics: Element not found for click tracking', '#non-existent' ); }); }); describe('flush()', () => { it('should flush event queue', async () => { await sdk.init(); jest.clearAllMocks(); await sdk.flush(); expect(mockEventQueue.flush).toHaveBeenCalled(); }); it('should handle flush errors', async () => { await sdk.init(); mockEventQueue.flush.mockRejectedValue(new Error('Flush failed')); jest.clearAllMocks(); await sdk.flush(); expect(mockErrorHandler.handle).toHaveBeenCalledWith( expect.any(Error), 'flush' ); }); }); describe('Privacy Management', () => { it('should update privacy consent', async () => { await sdk.init(); jest.clearAllMocks(); sdk.updatePrivacyConsent({ analytics: false, marketing: false }); expect(mockPrivacyManager.updateConsent).toHaveBeenCalledWith({ analytics: false, marketing: false }); }); it('should get privacy status', () => { const status = sdk.getPrivacyStatus(); expect(status).toEqual({ essential: true, functional: true, analytics: true, marketing: false }); expect(mockPrivacyManager.getConsentStatus).toHaveBeenCalled(); }); }); describe('Auto-tracking', () => { it('should set up auto-tracking on init', async () => { const clickListener = jest.fn(); document.addEventListener('click', clickListener); await sdk.init(); // Verify click tracking is enabled const button = document.createElement('button'); button.textContent = 'Test'; document.body.appendChild(button); button.click(); // Should have tracked the click expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click' }) ); button.remove(); document.removeEventListener('click', clickListener); }); it('should track form submissions', async () => { await sdk.init(); jest.clearAllMocks(); const form = document.createElement('form'); form.id = 'test-form'; form.action = '/submit'; form.method = 'post'; document.body.appendChild(form); // Prevent actual form submission form.addEventListener('submit', (e) => e.preventDefault()); form.dispatchEvent(new Event('submit', { bubbles: true })); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'form_submit', metadata: expect.objectContaining({ form_id: 'test-form', form_action: 'http://localhost/submit', form_method: 'post' }) }) ); form.remove(); }); it('should not auto-track if disabled', async () => { const noAutoTrackSdk = new TinyTapAnalyticsSDK({ ...mockConfig, enableAutoTracking: false }); await noAutoTrackSdk.init(); jest.clearAllMocks(); const button = document.createElement('button'); document.body.appendChild(button); button.click(); // Should only have page_view, not click const calls = mockEventQueue.enqueue.mock.calls; const clickEvents = calls.filter(call => call[0].event_type === 'click'); expect(clickEvents.length).toBe(0); button.remove(); }); }); describe('SPA Tracking', () => { it('should set up SPA tracking if detected', async () => { mockEnvironmentDetector.isSPA.mockReturnValue(true); const newSdk = new TinyTapAnalyticsSDK(mockConfig); await newSdk.init(); // Should have hooked into history API expect(history.pushState).toBeDefined(); }); }); describe('destroy()', () => { it('should clean up all resources', async () => { await sdk.init(); sdk.destroy(); expect(mockEventQueue.destroy).toHaveBeenCalled(); expect(mockPrivacyManager.destroy).toHaveBeenCalled(); expect(mockErrorHandler.destroy).toHaveBeenCalled(); }); it('should remove event listeners', async () => { await sdk.init(); const listenerCount = document.querySelectorAll('*').length; sdk.destroy(); // Verify cleanup happened expect(mockEventQueue.destroy).toHaveBeenCalled(); }); it('should handle destroy errors gracefully', async () => { await sdk.init(); mockEventQueue.destroy.mockImplementation(() => { throw new Error('Destroy failed'); }); sdk.destroy(); expect(console.error).toHaveBeenCalledWith( 'TinyTapAnalytics: Error during destroy', expect.any(Error) ); }); }); describe('Device Detection', () => { it('should detect desktop devices', async () => { Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', writable: true, configurable: true }); await sdk.init(); jest.clearAllMocks(); await sdk.track('test'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ device_type: 'desktop' }) }) ); }); it('should detect mobile devices', async () => { Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)', writable: true, configurable: true }); const mobileSdk = new TinyTapAnalyticsSDK(mockConfig); await mobileSdk.init(); jest.clearAllMocks(); await mobileSdk.track('test'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ device_type: 'mobile' }) }) ); }); it('should detect tablet devices', async () => { Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)', writable: true, configurable: true }); const tabletSdk = new TinyTapAnalyticsSDK(mockConfig); await tabletSdk.init(); jest.clearAllMocks(); await tabletSdk.track('test'); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ device_type: 'tablet' }) }) ); }); }); describe('Session Management', () => { it('should return session ID', () => { const sessionId = sdk.getSessionId(); expect(sessionId).toBeDefined(); expect(typeof sessionId).toBe('string'); expect(sessionId.length).toBeGreaterThan(0); }); it('should have unique session IDs for different SDK instances', () => { const sdk1 = new TinyTapAnalyticsSDK(mockConfig); const sdk2 = new TinyTapAnalyticsSDK(mockConfig); expect(sdk1.getSessionId()).not.toBe(sdk2.getSessionId()); }); }); describe('Micro-interaction Tracking', () => { it('should return null for stats when tracking not enabled', () => { const stats = sdk.getMicroInteractionStats(); expect(stats).toBeNull(); }); it('should return null for profile when tracking not enabled', () => { const profile = sdk.getMicroInteractionProfile(); expect(profile).toBeNull(); }); it('should warn when setting profile without tracking enabled', () => { const debugSdk = new TinyTapAnalyticsSDK({ ...mockConfig, debug: true }); debugSdk.setMicroInteractionProfile('balanced'); expect(console.warn).toHaveBeenCalledWith( expect.stringContaining('Micro-interaction tracking not enabled') ); }); it('should not throw when setting profile without tracking enabled', () => { expect(() => { sdk.setMicroInteractionProfile('minimal'); }).not.toThrow(); }); it('should start micro-interaction tracking when enabled', async () => { const microSdk = new TinyTapAnalyticsSDK({ ...mockConfig, enableMicroInteractionTracking: true, debug: true }); await microSdk.init(); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: Micro-interaction tracking started' ); }); it('should handle micro-interaction tracking errors', async () => { const microSdk = new TinyTapAnalyticsSDK({ ...mockConfig, enableMicroInteractions: true // Alternative property name }); // Mock MicroInteractionTracking to throw jest.spyOn(MicroInteractionTracking.prototype, 'start').mockImplementation(() => { throw new Error('Micro-interaction failed'); }); await microSdk.init(); expect(mockErrorHandler.handle).toHaveBeenCalledWith( expect.any(Error), 'micro_interaction_tracking' ); }); }); describe('Debug Logging', () => { let debugSdk: TinyTapAnalyticsSDK; beforeEach(() => { debugSdk = new TinyTapAnalyticsSDK({ ...mockConfig, debug: true }); }); it('should log when tracking disabled by privacy', async () => { mockPrivacyManager.canTrack.mockReturnValue(false); await debugSdk.init(); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: Essential tracking blocked by privacy settings, clearing consent data' ); }); it('should log when event blocked by privacy', async () => { await debugSdk.init(); mockPrivacyManager.canTrack.mockReturnValue(false); jest.clearAllMocks(); await debugSdk.track('analytics_event'); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: Event blocked by privacy settings', 'analytics_event' ); }); it('should log when flushing events', async () => { await debugSdk.init(); jest.clearAllMocks(); await debugSdk.flush(); expect(console.log).toHaveBeenCalledWith('TinyTapAnalytics: Events flushed'); }); it('should log when privacy consent updated', async () => { await debugSdk.init(); jest.clearAllMocks(); debugSdk.updatePrivacyConsent({ analytics: false }); expect(console.log).toHaveBeenCalledWith( 'TinyTapAnalytics: Privacy consent updated', { analytics: false } ); }); it('should log when SDK destroyed', async () => { await debugSdk.init(); jest.clearAllMocks(); debugSdk.destroy(); expect(console.log).toHaveBeenCalledWith('TinyTapAnalytics: SDK destroyed'); }); }); describe('Scroll Tracking', () => { it('should track scroll depth and only track each milestone once', async () => { jest.useFakeTimers(); const scrollSdk = new TinyTapAnalyticsSDK(mockConfig); await scrollSdk.init(); jest.clearAllMocks(); Object.defineProperty(window, 'innerHeight', { value: 1000, writable: true }); Object.defineProperty(document.documentElement, 'scrollHeight', { value: 5000, writable: true }); // Scroll past 25% multiple times Object.defineProperty(window, 'pageYOffset', { value: 1000, writable: true, configurable: true }); window.dispatchEvent(new Event('scroll')); jest.advanceTimersByTime(250); await Promise.resolve(); Object.defineProperty(window, 'pageYOffset', { value: 1100, writable: true, configurable: true }); window.dispatchEvent(new Event('scroll')); jest.advanceTimersByTime(250); await Promise.resolve(); // Should only track 25% once const scrollCalls = mockEventQueue.enqueue.mock.calls.filter( call => call[0].event_type === 'scroll' && call[0].metadata.depth === 25 ); expect(scrollCalls.length).toBeLessThanOrEqual(1); jest.useRealTimers(); }); }); describe('SPA Tracking', () => { it('should hook into history.pushState', async () => { mockEnvironmentDetector.isSPA.mockReturnValue(true); const spaSdk = new TinyTapAnalyticsSDK(mockConfig); await spaSdk.init(); jest.clearAllMocks(); // Trigger pushState history.pushState({}, '', '/new-page'); // Wait for setTimeout await new Promise(resolve => setTimeout(resolve, 150)); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'page_view' }) ); }); it('should hook into history.replaceState', async () => { mockEnvironmentDetector.isSPA.mockReturnValue(true); const spaSdk = new TinyTapAnalyticsSDK(mockConfig); await spaSdk.init(); jest.clearAllMocks(); // Trigger replaceState history.replaceState({}, '', '/replaced-page'); // Wait for setTimeout await new Promise(resolve => setTimeout(resolve, 150)); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'page_view' }) ); }); it('should track popstate events', async () => { mockEnvironmentDetector.isSPA.mockReturnValue(true); const spaSdk = new TinyTapAnalyticsSDK(mockConfig); await spaSdk.init(); jest.clearAllMocks(); // Trigger popstate window.dispatchEvent(new Event('popstate')); // Wait for setTimeout await new Promise(resolve => setTimeout(resolve, 150)); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'page_view' }) ); }); }); describe('CTA Class Detection', () => { it('should auto-track elements with btn class', async () => { await sdk.init(); jest.clearAllMocks(); const button = document.createElement('div'); button.className = 'primary-btn-large'; button.textContent = 'Click me'; document.body.appendChild(button); button.click(); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click' }) ); button.remove(); }); it('should auto-track elements with cta class', async () => { await sdk.init(); jest.clearAllMocks(); const div = document.createElement('div'); div.className = 'main-cta'; div.textContent = 'Sign up'; document.body.appendChild(div); div.click(); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click' }) ); div.remove(); }); it('should auto-track elements with checkout class', async () => { await sdk.init(); jest.clearAllMocks(); const div = document.createElement('div'); div.className = 'checkout-button'; div.textContent = 'Checkout'; document.body.appendChild(div); div.click(); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click' }) ); div.remove(); }); it('should auto-track elements with buy class', async () => { await sdk.init(); jest.clearAllMocks(); const div = document.createElement('div'); div.className = 'buy-now'; div.textContent = 'Buy Now'; document.body.appendChild(div); div.click(); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ event_type: 'click' }) ); div.remove(); }); }); describe('Event Queueing', () => { it('should create queue if it does not exist', async () => { const uninitializedSdk = new TinyTapAnalyticsSDK(mockConfig); // Ensure queue doesn't exist delete (window as any).__tinytapanalytics_queue; await uninitializedSdk.track('early_event', { data: 'test' }); expect((window as any).__tinytapanalytics_queue).toBeDefined(); expect((window as any).__tinytapanalytics_queue.length).toBe(1); }); it('should process queued events after initialization', async () => { const queueSdk = new TinyTapAnalyticsSDK(mockConfig); // Queue some events before init await queueSdk.track('event1', { data: '1' }); await queueSdk.track('event2', { data: '2' }); // Init should process the queue await queueSdk.init(); // Check that both queued events were processed const calls = mockEventQueue.enqueue.mock.calls; const event1 = calls.find(call => call[0].metadata?.data === '1'); const event2 = calls.find(call => call[0].metadata?.data === '2'); expect(event1).toBeDefined(); expect(event2).toBeDefined(); }); }); describe('Element Selector Fallback', () => { it('should use tagName fallback when no id or class', async () => { await sdk.init(); jest.clearAllMocks(); const span = document.createElement('span'); span.textContent = 'No ID or class'; document.body.appendChild(span); await sdk.trackClick(span); expect(mockEventQueue.enqueue).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ element: expect.stringMatching(/span/) }) }) ); span.remove(); }); }); describe('Beforeunload Handler', () => { it('should flush events on beforeunload', async () => { await sdk.init(); jest.clearAllMocks(); window.dispatchEvent(new Event('beforeunload')); expect(mockEventQueue.flush).toHaveBeenCalled(); }); }); describe('Destroy with MicroInteractionTracking', () => { it('should stop micro-interaction tracking on destroy', async () => { const microSdk = new TinyTapAnalyticsSDK({ ...mockConfig, enableMicroInteractionTracking: true }); await microSdk.init(); // Mock the microInteractionTracking instance const mockMicroTracking = { stop: jest.fn(), getStats: jest.fn(), getProfile: jest.fn(), setProfile: jest.fn() }; (microSdk as any).microInteractionTracking = mockMicroTracking; microSdk.destroy(); expect(mockMicroTracking.stop).toHaveBeenCalled(); }); }); describe('Error Handling', () => { it('should handle identify errors gracefully', async () => { const errorSdk = new TinyTapAnalyticsSDK(mockConfig); await errorSdk.init(); // Mock localStorage.setItem to throw an error const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); // Should not throw even if localStorage fails expect(() => errorSdk.identify('user-123', { email: 'test@example.com' })).not.toThrow(); setItemSpy.mockRestore(); }); it('should handle micro-interaction methods when tracking not initialized', () => { const sdk = new TinyTapAnalyticsSDK(mockConfig); // Call micro-interaction methods when tracking isn't initialized expect(() => sdk.setMicroInteractionProfile('minimal')).not.toThrow(); expect(() => sdk.getMicroInteractionProfile()).not.toThrow(); const stats = sdk.getMicroInteractionStats(); expect(stats).toBeNull(); }); }); });