const puppeteer = require('puppeteer'); const { getPuppeteerLaunchOptions } = require('./puppeteer_config'); // Test configuration const BASE_URL = 'http://127.0.0.1:5000'; const TEST_USER = `metrics_test_${Date.now()}`; const TEST_PASSWORD = 'TestPass123!'; // Colors for console output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; function log(message, type = 'info') { const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const typeColors = { 'info': colors.cyan, 'success': colors.green, 'error': colors.red, 'warning': colors.yellow, 'section': colors.blue }; const color = typeColors[type] || colors.reset; console.log(`${color}[${timestamp}] ${message}${colors.reset}`); } async function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function registerAndLogin(page) { log('šŸ“ Registering new user...', 'info'); await page.goto(`${BASE_URL}/auth/register`); await page.waitForSelector('#username'); await page.type('#username', TEST_USER); await page.type('#password', TEST_PASSWORD); await page.type('#confirm_password', TEST_PASSWORD); // Check the acknowledge checkbox const acknowledgeCheckbox = await page.$('#acknowledge'); if (acknowledgeCheckbox) { await acknowledgeCheckbox.click(); } await Promise.all([ page.waitForNavigation(), page.click('button[type="submit"]') ]); log('āœ… Registration successful', 'success'); } async function createResearch(page, query) { log(`šŸ”¬ Creating research: "${query}"`, 'info'); await page.goto(`${BASE_URL}/`); await page.waitForSelector('#query'); await page.evaluate((q) => { document.getElementById('query').value = q; }, query); // Wait for the start_research API response so we know submission completed // (passing or failing) before checking the URL — replaces a fixed 3s delay. const startResearchPromise = page.waitForResponse( (response) => response.url().includes('/api/start_research'), { timeout: 15000 } ).catch(() => null); await page.click('#start-research-btn'); log('ā³ Waiting for research submission...', 'info'); const response = await startResearchPromise; if (response) { log(`šŸ“„ start_research → ${response.status()}`, 'info'); } // Give the SPA a tiny moment to navigate; check URL right after. await page.waitForFunction( () => location.pathname.includes('/progress/') || location.pathname.includes('/research/'), { timeout: 5000 } ).catch(() => {}); const currentUrl = page.url(); if (currentUrl.includes('/progress/') || currentUrl.includes('/research/')) { log('āœ… Research submitted successfully', 'success'); return; } log('āš ļø Research did not navigate to progress/research page', 'warning'); } async function testMetricsDashboard() { const browser = await puppeteer.launch(getPuppeteerLaunchOptions()); const page = await browser.newPage(); // Enable request interception to monitor API calls await page.setRequestInterception(true); const apiCalls = []; page.on('request', (request) => { const url = request.url(); if (url.includes('/api/metrics') || url.includes('/api/')) { apiCalls.push({ url, method: request.method(), timestamp: new Date().toISOString() }); } request.continue(); }); // Set console log handler page.on('console', msg => { if (msg.type() === 'error' && !msg.text().includes('favicon')) { log(`Browser console error: ${msg.text()}`, 'error'); } }); try { // Setup: Register and create some research await registerAndLogin(page); // In CI, we'll start one quick research just to test the flow log('\n=== CREATING TEST DATA ===', 'section'); // Only create one simple research to avoid timeouts await createResearch(page, 'What is 2+2?'); // Navigate to metrics dashboard log('\n=== TESTING METRICS DASHBOARD ===', 'section'); await page.goto(`${BASE_URL}/metrics`); // Wait for page to load - check multiple possible selectors. If // neither the expected container nor the URL matches, the metrics // dashboard is broken and we want the test to fail loudly rather // than continue and emit downstream "may not be working" warnings. try { await page.waitForSelector('.metrics-container, #metrics-dashboard, .container h1, [data-page="metrics"]', { timeout: 10000 }); log('āœ… Metrics page loaded', 'success'); } catch { const currentUrl = page.url(); if (currentUrl.includes('/metrics')) { log('āœ… On metrics page (by URL)', 'success'); } else { throw new Error( `Metrics dashboard did not load: no expected selector and URL is ${currentUrl}. ` + 'Either the route is broken or the page renders without any of the known root selectors.' ); } } // Take screenshot if not in headless mode const isHeadless = process.env.HEADLESS !== 'false'; if (!isHeadless) { await page.screenshot({ path: './metrics-dashboard-test.png', fullPage: true }); log('šŸ“ø Screenshot saved: ./metrics-dashboard-test.png', 'info'); } // Wait for the metrics API response (or a short ceiling) instead of // a fixed 3s delay. The metrics page issues several /api/metrics // calls on load; we only need to see one land before we assert. await page.waitForResponse( (response) => response.url().includes('/api/metrics'), { timeout: 10000 } ).catch(() => { log('ā„¹ļø No /api/metrics response within 10s — page may have no data yet', 'info'); }); // Check if metrics API was called const metricsApiCalls = apiCalls.filter(call => call.url.includes('/api/metrics')); log(`šŸ“Š Metrics API calls made: ${metricsApiCalls.length}`, 'info'); if (metricsApiCalls.length === 0) { log('āš ļø No metrics API calls detected (expected in CI with no data)', 'warning'); } else { log('āœ… Metrics API was called', 'success'); } // Check page content const metricsData = await page.evaluate(() => { const data = { title: document.title, hasCharts: !!document.querySelector('canvas, .chart-container, .chart'), sections: [], stats: {}, errors: [], gridLayout: null }; // Find metric sections const sections = document.querySelectorAll('.metric-section, .ldr-card, .panel'); sections.forEach(section => { const title = section.querySelector('h2, h3, .card-title, .panel-title'); if (title) { data.sections.push(title.textContent.trim()); } }); // Find statistic values const statElements = document.querySelectorAll('.stat-value, .metric-value, [data-metric]'); statElements.forEach(el => { const label = el.getAttribute('data-metric') || el.previousElementSibling?.textContent || 'unknown'; data.stats[label] = el.textContent.trim(); }); // Check for error messages const errors = document.querySelectorAll('.error, .alert-danger'); errors.forEach(error => { data.errors.push(error.textContent.trim()); }); // Check for loading indicators still visible data.hasLoadingIndicators = !!document.querySelector('.loading, .spinner'); // Check grid layout for metrics const metricsGrid = document.querySelector('.ldr-metrics-grid, .metrics-grid'); if (metricsGrid) { const gridStyle = window.getComputedStyle(metricsGrid); const cards = metricsGrid.querySelectorAll('.ldr-metric-card, .metric-card'); data.gridLayout = { found: true, display: gridStyle.display, gridTemplateColumns: gridStyle.gridTemplateColumns, gap: gridStyle.gap, cardCount: cards.length, cardsVisible: Array.from(cards).map(card => ({ visible: card.offsetParent !== null, width: card.offsetWidth, hasValue: !!card.querySelector('.ldr-metric-value, .metric-value') })) }; } else { data.gridLayout = { found: false }; } return data; }); log('šŸ“Š Metrics page content:', 'info'); log(` - Title: ${metricsData.title}`, 'info'); log(` - Sections found: ${metricsData.sections.length}`, 'info'); log(` - Has charts: ${metricsData.hasCharts}`, 'info'); log(` - Statistics found: ${Object.keys(metricsData.stats).length}`, 'info'); // Log grid layout info if (metricsData.gridLayout) { if (metricsData.gridLayout.found) { log('šŸ“ Grid Layout:', 'info'); log(` - Display: ${metricsData.gridLayout.display}`, 'info'); log(` - Grid columns: ${metricsData.gridLayout.gridTemplateColumns}`, 'info'); log(` - Gap: ${metricsData.gridLayout.gap}`, 'info'); log(` - Cards found: ${metricsData.gridLayout.cardCount}`, 'info'); const visibleCards = metricsData.gridLayout.cardsVisible.filter(c => c.visible).length; log(` - Cards visible: ${visibleCards}/${metricsData.gridLayout.cardCount}`, 'info'); // Check if cards are in grid (side by side) if (metricsData.gridLayout.display === 'grid' && visibleCards > 1) { const widths = metricsData.gridLayout.cardsVisible.map(c => c.width); const uniqueWidths = [...new Set(widths)]; if (uniqueWidths.length === 1 && widths[0] < 600) { log('āœ… Metrics cards are displayed in grid layout (side by side)', 'success'); } else { log('āš ļø Metrics cards may not be in proper grid layout', 'warning'); } } } else { // The page-level metrics grid container is what holds the // primary stat cards. If it's missing, the dashboard is // structurally broken — fail rather than warn. throw new Error( 'Metrics grid container (.ldr-metrics-grid / .metrics-grid) not found on /metrics. ' + 'The dashboard layout has regressed.' ); } } if (metricsData.sections.length > 0) { log('šŸ“‹ Sections:', 'info'); metricsData.sections.forEach(section => { log(` - ${section}`, 'info'); }); } if (metricsData.errors.length > 0) { log('āŒ Errors found on page:', 'error'); metricsData.errors.forEach(error => { log(` - ${error}`, 'error'); }); } // Test period selector if available log('\n=== TESTING PERIOD SELECTOR ===', 'section'); const periodSelector = await page.$('.period-selector, select[name="period"], #period-select'); if (periodSelector) { // Wait for the period change to trigger an API call carrying the // new period — replaces a fixed 2s delay. If nothing fires within // 5s, we fall through to the "may not be working" branch. const periodApiPromise = page.waitForResponse( (response) => response.url().includes('period=7d'), { timeout: 5000 } ).catch(() => null); await page.select('.period-selector, select[name="period"], #period-select', '7d'); await periodApiPromise; // Check if API was called with new period const recentApiCalls = apiCalls.filter(call => call.url.includes('period=7d') && new Date() - new Date(call.timestamp) < 3000 ); if (recentApiCalls.length > 0) { log('āœ… Period selector works - API called with new period', 'success'); } else { // The selector is rendered but changing it didn't trigger an // API call with the new period — the period filter is broken. throw new Error( 'Period selector exists but changing it did not trigger an API call with period=7d. ' + 'The filter handler has regressed.' ); } } // Test sub-pages log('\n=== TESTING SUB-PAGES ===', 'section'); // Test cost analytics page const costLink = await page.$('a[href*="/costs"], a[href*="/cost-analytics"]'); if (costLink) { await costLink.click(); await page.waitForNavigation(); await page.waitForSelector('.cost-analytics, #cost-dashboard', { timeout: 5000 }).catch(() => {}); const costPageUrl = page.url(); if (costPageUrl.includes('cost')) { log('āœ… Cost analytics page accessible', 'success'); // Check for cost data const hasCostData = await page.evaluate(() => { return document.body.textContent.includes('$') || document.body.textContent.includes('cost') || document.body.textContent.includes('Cost'); }); if (hasCostData) { log(' - Cost data present', 'info'); } } } // Go back to main metrics; wait for the dashboard to actually render // (replaces a fixed 2s delay). await page.goto(`${BASE_URL}/metrics`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector( '.metrics-container, #metrics-dashboard, .container h1, [data-page="metrics"]', { timeout: 10000 } ).catch(() => {}); // Test star reviews page const reviewsLink = await page.$('a[href*="/star-reviews"], a[href*="/reviews"]'); if (reviewsLink) { await reviewsLink.click(); await page.waitForNavigation(); await page.waitForSelector('.star-reviews, #reviews-dashboard', { timeout: 5000 }).catch(() => {}); const reviewsPageUrl = page.url(); if (reviewsPageUrl.includes('review')) { log('āœ… Star reviews page accessible', 'success'); } } // Test rate limiting metrics log('\n=== TESTING RATE LIMITING METRICS ===', 'section'); await page.goto(`${BASE_URL}/metrics`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector( '.metrics-container, #metrics-dashboard, .container h1, [data-page="metrics"]', { timeout: 10000 } ).catch(() => {}); const rateLimitingData = await page.evaluate(() => { const elements = Array.from(document.querySelectorAll('*')); return elements.some(el => el.textContent.includes('rate limit') || el.textContent.includes('Rate Limit') ); }); if (rateLimitingData) { log('āœ… Rate limiting metrics present', 'success'); } // Check for charts/visualizations log('\n=== TESTING VISUALIZATIONS ===', 'section'); const hasVisualizations = await page.evaluate(() => { return { canvas: document.querySelectorAll('canvas').length, svg: document.querySelectorAll('svg').length, chartContainers: document.querySelectorAll('.chart, .chart-container').length }; }); log(`šŸ“Š Visualizations found:`, 'info'); log(` - Canvas elements: ${hasVisualizations.canvas}`, 'info'); log(` - SVG elements: ${hasVisualizations.svg}`, 'info'); log(` - Chart containers: ${hasVisualizations.chartContainers}`, 'info'); // Final API call summary log('\n=== API CALLS SUMMARY ===', 'section'); const apiEndpoints = [...new Set(apiCalls.map(call => { const url = new URL(call.url); return url.pathname; }))]; log(`šŸ“” Unique API endpoints called: ${apiEndpoints.length}`, 'info'); apiEndpoints.forEach(endpoint => { log(` - ${endpoint}`, 'info'); }); log('\nāœ… Metrics dashboard test completed successfully!', 'success'); } catch (error) { log(`\nāŒ Test failed: ${error.message}`, 'error'); throw error; } finally { await browser.close(); } } // Run the test testMetricsDashboard().catch(error => { console.error('Test execution failed:', error); process.exit(1); });