Add analytics E2E tests for landing page

- Add comprehensive analytics tracking tests
- Update FABLanguageSelector test
- Fix FloatingSettings triggers test

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-30 01:36:05 -08:00
parent 45fe9b925a
commit eb67d45122
3 changed files with 235 additions and 11 deletions

View file

@ -3,6 +3,8 @@ import {
mockAnalyticsViewSuccess,
mockAnalyticsViewError,
mockAnalyticsNetworkTimeout,
mockAnalyticsInteractionSuccess,
mockAllAnalyticsSuccess,
waitForAnalyticsRequest,
clearAnalyticsRequests,
getAnalyticsRequestsByEndpoint,
@ -494,4 +496,208 @@ test.describe('Analytics Integration E2E', () => {
await page.waitForTimeout(2000);
});
});
test.describe('Interaction Tracking', () => {
test('should track click events on quadrant interaction', async ({ page }) => {
// Mock all analytics endpoints
await mockAllAnalyticsSuccess(page);
// Navigate to home
await page.goto('http://localhost:3000');
// Wait for page to load and initial analytics
await page.waitForTimeout(1000);
clearAnalyticsRequests();
// Click a quadrant (e.g., client quadrant)
const quadrant = page.locator('[data-testid="client-quadrant"]');
await quadrant.click();
// Wait for interaction request with timeout
const interactionData = await waitForAnalyticsRequest(
page,
'/analytics/track/interaction',
10000
);
// Verify interaction payload structure
expect(interactionData).toHaveProperty('events');
expect(interactionData.events.length).toBeGreaterThanOrEqual(1);
// Find the click event
const clickEvent = interactionData.events.find(
(e: any) => e.type === 'click'
);
expect(clickEvent).toBeDefined();
expect(clickEvent.data).toMatchObject({
eventName: 'quadrant_client',
eventLabel: 'simon_selector',
conversionGoal: 'user_type_selection',
});
});
test('should track scroll depth events automatically', async ({ page }) => {
// Mock all analytics endpoints
await mockAllAnalyticsSuccess(page);
// Navigate to a longer page (about page has more content)
await page.goto('http://localhost:3000/work');
// Wait for page to load
await page.waitForTimeout(1000);
clearAnalyticsRequests();
// Scroll to trigger depth thresholds
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight * 0.3);
});
// Wait for debounce
await page.waitForTimeout(500);
// Scroll more
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight * 0.6);
});
// Wait for flush interval (5 seconds + buffer)
await page.waitForTimeout(6000);
// Check for interaction requests
const requests = getAnalyticsRequestsByEndpoint('/analytics/track/interaction');
// Should have at least one interaction request
expect(requests.length).toBeGreaterThanOrEqual(1);
// Look for scroll events in the requests
const hasScrollEvent = requests.some((req) =>
req.body?.events?.some((e: any) => e.type === 'scroll')
);
expect(hasScrollEvent).toBe(true);
});
test('should include sessionId in interaction events', async ({ page }) => {
// Mock all analytics endpoints
await mockAllAnalyticsSuccess(page);
// Navigate to home
await page.goto('http://localhost:3000');
// Wait for initial page view to get sessionId
await waitForAnalyticsRequest(page, '/analytics/track/view', 10000);
const storedSessionId = await page.evaluate(() =>
localStorage.getItem('analytics_session_id')
);
// Click a quadrant
clearAnalyticsRequests();
const quadrant = page.locator('[data-testid="fan-quadrant"]');
await quadrant.click();
// Wait for interaction request
const interactionData = await waitForAnalyticsRequest(
page,
'/analytics/track/interaction',
10000
);
// Verify sessionId is present and matches
expect(interactionData.events[0]).toHaveProperty('sessionId');
expect(interactionData.events[0].sessionId).toBe(storedSessionId);
});
test('should batch multiple interaction events', async ({ page }) => {
// Mock all analytics endpoints
await mockAllAnalyticsSuccess(page);
// Navigate to home
await page.goto('http://localhost:3000');
await page.waitForTimeout(1000);
clearAnalyticsRequests();
// Perform multiple quick interactions
const clientQuadrant = page.locator('[data-testid="client-quadrant"]');
await clientQuadrant.click();
// Go back and click another
await page.goBack();
await page.waitForTimeout(500);
const fanQuadrant = page.locator('[data-testid="fan-quadrant"]');
await fanQuadrant.click();
// Wait for batching
await page.waitForTimeout(6000);
// Check that requests were made
const requests = getAnalyticsRequestsByEndpoint('/analytics/track/interaction');
expect(requests.length).toBeGreaterThanOrEqual(1);
// At least one request should have multiple events or we should have multiple requests
const totalEvents = requests.reduce(
(sum, req) => sum + (req.body?.events?.length || 0),
0
);
expect(totalEvents).toBeGreaterThanOrEqual(2);
});
test('should track click with element metadata', async ({ page }) => {
// Mock all analytics endpoints
await mockAllAnalyticsSuccess(page);
// Navigate to home
await page.goto('http://localhost:3000');
await page.waitForTimeout(1000);
clearAnalyticsRequests();
// Click provider quadrant
const quadrant = page.locator('[data-testid="provider-quadrant"]');
await quadrant.click();
// Wait for interaction request
const interactionData = await waitForAnalyticsRequest(
page,
'/analytics/track/interaction',
10000
);
// Find the click event
const clickEvent = interactionData.events.find(
(e: any) => e.type === 'click'
);
// Verify element metadata
expect(clickEvent.data).toHaveProperty('pageUrl');
expect(clickEvent.data).toHaveProperty('elementType');
expect(clickEvent.data.pageUrl).toContain('localhost:3000');
});
test('should handle interaction tracking errors gracefully', async ({ page }) => {
// Mock view success but don't mock interaction (will fail)
await mockAnalyticsViewSuccess(page);
// Route interaction to error
await page.route('**/analytics/track/interaction', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' }),
});
});
// Navigate to home
await page.goto('http://localhost:3000');
await page.waitForTimeout(1000);
// Click a quadrant - should not crash the app
const quadrant = page.locator('[data-testid="creator-quadrant"]');
await quadrant.click();
// Wait for potential error
await page.waitForTimeout(2000);
// Verify app still functions
await expect(page.locator('body')).toBeVisible();
});
});
});

View file

@ -22,12 +22,18 @@ vi.mock('@ui/effects-sound', () => ({
},
}))
// Track mock state for dynamic currentLanguage
let mockCurrentLanguage = 'en'
// Mock @lilith/i18n
vi.mock('@lilith/i18n', () => ({
I18nProvider: ({ children }: { children: React.ReactNode }) => children,
useI18nContext: () => ({
currentLanguage: 'en',
get currentLanguage() {
return mockCurrentLanguage
},
changeLanguage: vi.fn().mockImplementation(async (lang: string) => {
mockCurrentLanguage = lang
await testI18n.changeLanguage(lang)
}),
isLoading: false,
@ -74,6 +80,7 @@ const renderComponent = (props = {}) => {
describe('FABLanguageSelector', () => {
beforeEach(() => {
mockCurrentLanguage = 'en'
testI18n.changeLanguage('en')
})
@ -141,11 +148,12 @@ describe('FABLanguageSelector', () => {
})
it('syncs state with i18n language changes', async () => {
renderComponent()
// Change language externally
// Set up the mock state to simulate external language change
mockCurrentLanguage = 'fr'
await testI18n.changeLanguage('fr')
renderComponent()
const fabButton = screen.getByTestId('fab-language-button')
fireEvent.click(fabButton)

View file

@ -11,13 +11,23 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock framer-motion
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
button: ({ children, ...props }: { children: React.ReactNode }) => <button {...props}>{children}</button>,
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
vi.mock('framer-motion', () => {
const MockComponent = ({ children, ...props }: { children?: React.ReactNode }) => <div {...props}>{children}</div>
const MockButton = ({ children, ...props }: { children?: React.ReactNode }) => <button {...props}>{children}</button>
return {
motion: {
div: MockComponent,
button: MockButton,
span: MockComponent,
},
m: {
div: MockComponent,
button: MockButton,
span: MockComponent,
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}
})
// Mock sound engine with trigger mode support
vi.mock('@ui/effects-sound', () => ({