@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
text/typescript
/**
* 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();
});
});
});