feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)
Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth
Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.
Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00
|
|
|
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'),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
2025-12-29 21:10:12 -08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
});
|
feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)
Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth
Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.
Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00
|
|
|
});
|