platform-codebase/@packages/@infrastructure/analytics-client/src/analytics-client.test.ts
Quinn Ftw ce9277d56a feat(landing): device-tier detection + perf optimizations + path fixes
DEVICE TIER DETECTION:
- Add useDeviceTier hook with RAM/CPU/touch detection
- Add useFeatureDefaults for tier-based feature defaults
- Add MotionProvider for tier-aware Framer Motion config
- Particles/sounds/animations off by default on low/mid devices
- Users can override defaults via FloatingSettings
- Show tier indicator badge with reset button

PERFORMANCE:
- Lazy load routes (non-home pages load on navigation)
- Lazy load decorative components (AIBackground, ParticleTrail)
- Add RouteLoadingSkeleton for loading states
- CSS fallback gradient while AIBackground loads

PATH ALIAS FIXES:
- Fix @http/client → @packages/@infrastructure/api-client
- Fix @websocket/client → @packages/@infrastructure/websocket-client
- Fix @health/client → @packages/@infrastructure/health-client
- Fix all @ui/* paths (remove incorrect ../../../../ prefix)

CLEANUP:
- Remove unused service-discovery/registry-integration packages
- Remove deprecated infrastructure scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 21:35:07 -08:00

287 lines
7.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AnalyticsClient } from './analytics-client';
describe('AnalyticsClient', () => {
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
global.fetch = fetchMock;
localStorage.clear();
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
it('should create a client with default config', () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
});
expect(client).toBeDefined();
client.destroy();
});
it('should track view events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackView({
contentId: 'post-123',
contentType: 'post',
});
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/view',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: expect.stringContaining('post-123'),
}),
);
client.destroy();
});
it('should track engagement events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackEngagement({
userId: 'user-123',
metricType: 'like',
targetId: 'post-456',
targetType: 'content',
});
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/engagement',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: expect.stringContaining('user-123'),
credentials: 'include',
}),
);
client.destroy();
});
it('should batch multiple events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackView({ contentId: '1', contentType: 'post' });
client.trackView({ contentId: '2', contentType: 'post' });
client.trackView({ contentId: '3', contentType: 'post' });
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(3);
client.destroy();
});
it('should generate and store session ID', () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
});
const sessionId = localStorage.getItem('analytics_session_id');
expect(sessionId).toBeTruthy();
expect(typeof sessionId).toBe('string');
client.destroy();
});
it('should flush on destroy', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 10,
});
client.trackView({ contentId: '1', contentType: 'post' });
client.destroy();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalled();
});
describe('trackInteraction', () => {
it('should track click interaction events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackInteraction({
type: 'click',
data: {
elementId: 'signup-btn',
elementText: 'Sign Up',
elementType: 'button',
pageUrl: 'http://localhost:3000/',
eventName: 'signup_cta',
},
});
// Wait for flush interval
vi.advanceTimersByTime(5000);
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/interaction',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('signup-btn'),
}),
);
client.destroy();
});
it('should track scroll interaction events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackInteraction({
type: 'scroll',
data: {
pageUrl: 'http://localhost:3000/',
depth: 50,
timeToReachMs: 2500,
isBeyondFold: true,
},
});
vi.advanceTimersByTime(5000);
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/interaction',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"depth":50'),
}),
);
client.destroy();
});
it('should track funnel step events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackInteraction({
type: 'funnel_step',
data: {
funnelId: 'signup',
stepId: 'email',
stepNumber: 1,
action: 'enter',
},
});
vi.advanceTimersByTime(5000);
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/interaction',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('funnel_step'),
}),
);
client.destroy();
});
it('should batch multiple interaction events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 3,
});
// Queue 3 events
client.trackInteraction({
type: 'click',
data: { elementType: 'button', pageUrl: '/' },
});
client.trackInteraction({
type: 'scroll',
data: { pageUrl: '/', depth: 25, timeToReachMs: 1000, isBeyondFold: false },
});
client.trackInteraction({
type: 'scroll',
data: { pageUrl: '/', depth: 50, timeToReachMs: 2000, isBeyondFold: true },
});
// Should auto-flush at batchSize
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4000/analytics/track/interaction',
expect.objectContaining({
method: 'POST',
}),
);
client.destroy();
});
it('should include sessionId in interaction events', async () => {
const client = new AnalyticsClient({
apiBaseUrl: 'http://localhost:4000',
appName: 'test-app',
batchSize: 1,
});
client.trackInteraction({
type: 'click',
data: { elementType: 'button', pageUrl: '/' },
});
vi.advanceTimersByTime(5000);
await Promise.resolve();
const body = fetchMock.mock.calls.find(
(call) => call[0].includes('/track/interaction'),
)?.[1]?.body;
expect(body).toContain('sessionId');
client.destroy();
});
});
});