feature flags

This commit is contained in:
2026-01-07 22:05:53 +01:00
parent 1b63fa646c
commit 606b64cec7
530 changed files with 2092 additions and 2943 deletions

View File

@@ -5,6 +5,83 @@ import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
// Wait for API to be ready with seeded data before running tests
test.beforeAll(async ({ request }) => {
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
console.log('[SETUP] Waiting for API to be ready...');
// Poll the API until it returns data (indicating seeding is complete)
const maxAttempts = 60;
const interval = 1000; // 1 second
for (let i = 0; i < maxAttempts; i++) {
try {
// Try to fetch total drivers count - this endpoint should return > 0 after seeding
const response = await request.get(`${API_BASE_URL}/drivers/total-drivers`);
if (response.ok()) {
const data = await response.json();
// Check if we have actual drivers (count > 0)
if (data && data.totalDrivers && data.totalDrivers > 0) {
console.log(`[SETUP] API is ready with ${data.totalDrivers} drivers`);
return;
}
}
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: API not ready yet (status: ${response.status()})`);
} catch (error) {
console.log(`[SETUP] Attempt ${i + 1}/${maxAttempts}: ${error.message}`);
}
// Wait before next attempt
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error('[SETUP] API failed to become ready with seeded data within timeout');
});
/**
* Helper to fetch feature flags from the API
* Uses Playwright request context for compatibility across environments
*/
async function fetchFeatureFlags(request: import('@playwright/test').APIRequestContext): Promise<{ features: Record<string, string>; timestamp: string }> {
const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
const featuresUrl = `${apiBaseUrl}/features`;
try {
const response = await request.get(featuresUrl);
expect(response.ok()).toBe(true);
const data = await response.json();
return data;
} catch (error) {
console.error(`[FEATURE FLAGS] Failed to fetch from ${featuresUrl}:`, error);
throw error;
}
}
/**
* Helper to compute enabled flags from feature config
*/
function getEnabledFlags(featureData: { features: Record<string, string> }): string[] {
if (!featureData.features || typeof featureData.features !== 'object') {
return [];
}
return Object.entries(featureData.features)
.filter(([, value]) => value === 'enabled')
.map(([flag]) => flag);
}
/**
* Helper to check if a specific flag is enabled
*/
function isFeatureEnabled(featureData: { features: Record<string, string> }, flag: string): boolean {
return featureData.features?.[flag] === 'enabled';
}
test.describe('Website Pages - TypeORM Integration', () => {
let routeManager: WebsiteRouteManager;
@@ -611,4 +688,148 @@ test.describe('Website Pages - TypeORM Integration', () => {
expect(detailBodyText?.length).toBeGreaterThan(50);
});
// ==================== FEATURE FLAG TESTS ====================
// These tests validate API-driven feature flags
test('features endpoint returns valid contract and reachable from API', async ({ request }) => {
// Contract test: verify /features endpoint returns correct shape
const featureData = await fetchFeatureFlags(request);
// Verify contract: { features: object, timestamp: string }
expect(featureData).toHaveProperty('features');
expect(featureData).toHaveProperty('timestamp');
// Verify features is an object
expect(typeof featureData.features).toBe('object');
expect(featureData.features).not.toBeNull();
// Verify timestamp is a string (ISO format)
expect(typeof featureData.timestamp).toBe('string');
expect(featureData.timestamp.length).toBeGreaterThan(0);
// Verify at least one feature exists (basic sanity check)
const featureKeys = Object.keys(featureData.features);
expect(featureKeys.length).toBeGreaterThan(0);
// Verify all feature values are valid states
const validStates = ['enabled', 'disabled', 'coming_soon', 'hidden'];
Object.values(featureData.features).forEach(value => {
expect(validStates).toContain(value);
});
console.log(`[FEATURE TEST] API features endpoint verified: ${featureKeys.length} flags loaded`);
});
test('conditional UI rendering based on feature flags', async ({ page, request }) => {
// Fetch current feature flags from API
const featureData = await fetchFeatureFlags(request);
const enabledFlags = getEnabledFlags(featureData);
console.log(`[FEATURE TEST] Enabled flags: ${enabledFlags.join(', ')}`);
// Test 1: Verify beta features are conditionally rendered
// Check if beta.newUI feature affects UI
const betaNewUIEnabled = isFeatureEnabled(featureData, 'beta.newUI');
// Navigate to a page that might have beta features
const response = await page.goto(`${WEBSITE_BASE_URL}/dashboard`);
expect(response?.ok()).toBe(true);
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
// If beta.newUI is enabled, we should see beta UI elements
// If disabled, beta elements should be absent
if (betaNewUIEnabled) {
console.log('[FEATURE TEST] beta.newUI is enabled - checking for beta UI elements');
// Beta UI might have specific markers - check for common beta indicators
const hasBetaIndicators = bodyText?.includes('beta') ||
bodyText?.includes('Beta') ||
bodyText?.includes('NEW') ||
bodyText?.includes('experimental');
// Beta features may or may not be visible depending on implementation
// This test validates the flag is being read correctly
// We don't assert on hasBetaIndicators since beta UI may not be implemented yet
console.log(`[FEATURE TEST] Beta indicators found: ${hasBetaIndicators}`);
} else {
console.log('[FEATURE TEST] beta.newUI is disabled - verifying beta UI is absent');
// If disabled, ensure no beta indicators are present
const hasBetaIndicators = bodyText?.includes('beta') ||
bodyText?.includes('Beta') ||
bodyText?.includes('experimental');
// Beta UI should not be visible when disabled
expect(hasBetaIndicators).toBe(false);
}
// Test 2: Verify platform features are enabled
const platformFeatures = ['platform.leagues', 'platform.teams', 'platform.drivers'];
platformFeatures.forEach(flag => {
const isEnabled = isFeatureEnabled(featureData, flag);
expect(isEnabled).toBe(true); // Should be enabled in test environment
});
});
test('feature flag state drives UI behavior', async ({ page, request }) => {
// This test validates that feature flags actually control UI visibility
const featureData = await fetchFeatureFlags(request);
// Test sponsor management feature
const sponsorManagementEnabled = isFeatureEnabled(featureData, 'sponsors.management');
// Navigate to sponsor-related area
const response = await page.goto(`${WEBSITE_BASE_URL}/sponsor/dashboard`);
// If sponsor management is disabled, we should be redirected or see access denied
if (!sponsorManagementEnabled) {
// Should redirect away or show access denied
const currentUrl = page.url();
const isRedirected = !currentUrl.includes('/sponsor/dashboard');
if (isRedirected) {
console.log('[FEATURE TEST] Sponsor management disabled - user redirected as expected');
} else {
// If not redirected, should show access denied message
const bodyText = await page.textContent('body');
const hasAccessDenied = bodyText?.includes('disabled') ||
bodyText?.includes('unavailable') ||
bodyText?.includes('not available');
expect(hasAccessDenied).toBe(true);
}
} else {
// Should be able to access sponsor dashboard
expect(response?.ok()).toBe(true);
console.log('[FEATURE TEST] Sponsor management enabled - dashboard accessible');
}
});
test('feature flags are consistent across environments', async ({ request }) => {
// This test validates that the same feature endpoint works in both local dev and docker e2e
const featureData = await fetchFeatureFlags(request);
// Verify the API base URL is correctly resolved
const apiBaseUrl = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
console.log(`[FEATURE TEST] Using API base URL: ${apiBaseUrl}`);
// Verify we got valid data
expect(featureData.features).toBeDefined();
expect(Object.keys(featureData.features).length).toBeGreaterThan(0);
// In test environment, core features should be enabled
const requiredFeatures = [
'platform.dashboard',
'platform.leagues',
'platform.teams',
'platform.drivers',
'platform.races',
'platform.leaderboards'
];
requiredFeatures.forEach(flag => {
const isEnabled = isFeatureEnabled(featureData, flag);
expect(isEnabled).toBe(true);
});
console.log('[FEATURE TEST] All required platform features are enabled');
});
});