#!/usr/bin/env node /** * Metrics Dashboard UI Tests * * Tests for the metrics dashboard, cost analytics, star reviews, and link analytics pages. * * Run: node test_metrics_dashboard_ci.js */ const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib'); // ============================================================================ // Metrics Dashboard Tests // ============================================================================ const MetricsDashboardTests = { async metricsDashboardLoads(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { // Page-specific container rendered server-side by pages/metrics.html: //
const container = document.querySelector('#metrics.ldr-page'); const header = container?.querySelector('.ldr-page-header h1'); return { hasContainer: !!container, headerText: header?.textContent?.trim() }; }); const passed = result.hasContainer; return { passed, message: passed ? `Metrics dashboard loaded (header: "${result.headerText}")` : 'Metrics dashboard container (#metrics.ldr-page) not found — wrong page loaded' }; }, async metricsOverviewCards(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { const cards = document.querySelectorAll('.metric-card, .stats-card, .card, .ldr-card'); const cardTexts = Array.from(cards).map(c => c.textContent?.toLowerCase() || ''); return { cardCount: cards.length, hasTokenCard: cardTexts.some(t => t.includes('token') || t.includes('usage')), hasSearchCard: cardTexts.some(t => t.includes('search') || t.includes('queries')), hasSatisfactionCard: cardTexts.some(t => t.includes('satisfaction') || t.includes('rating')), hasCostCard: cardTexts.some(t => t.includes('cost') || t.includes('spend')) }; }); if (result.cardCount === 0) { return { passed: null, skipped: true, message: 'No metric cards found on dashboard' }; } return { passed: result.hasTokenCard || result.hasSearchCard || result.hasSatisfactionCard, message: `Found ${result.cardCount} cards (tokens=${result.hasTokenCard}, search=${result.hasSearchCard}, satisfaction=${result.hasSatisfactionCard})` }; }, async periodFilterDropdown(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { const periodFilter = document.querySelector( 'select[name*="period"], ' + '#period-filter, ' + '.period-filter, ' + 'select[id*="period"]' ); if (!periodFilter) { // Check for button-based filter const buttons = document.querySelectorAll('.period-btn, .time-filter button, [data-period]'); if (buttons.length > 0) { return { exists: true, isButtonBased: true, buttonCount: buttons.length, options: Array.from(buttons).map(b => b.textContent?.trim()).slice(0, 6) }; } return { exists: false }; } const options = Array.from(periodFilter.options); return { exists: true, isButtonBased: false, optionCount: options.length, options: options.map(o => o.text).slice(0, 6) }; }); if (!result.exists) { return { passed: null, skipped: true, message: 'No period filter found on metrics page' }; } return { passed: true, message: result.isButtonBased ? `Period filter (buttons): ${result.options.join(', ')}` : `Period filter dropdown: ${result.options.join(', ')}` }; }, async modeFilterDropdown(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { const modeFilter = document.querySelector( 'select[name*="mode"], ' + '#mode-filter, ' + '.mode-filter' ); if (!modeFilter) { const buttons = document.querySelectorAll('.mode-btn, [data-mode]'); if (buttons.length > 0) { return { exists: true, isButtonBased: true, options: Array.from(buttons).map(b => b.textContent?.trim()) }; } return { exists: false }; } return { exists: true, options: Array.from(modeFilter.options).map(o => o.text) }; }); if (!result.exists) { return { passed: null, skipped: true, message: 'No mode filter found' }; } return { passed: true, message: `Mode filter: ${result.options.join(', ')}` }; }, async tokenUsageChart(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); await delay(1000); // Wait for charts to render const result = await page.evaluate(() => { const charts = document.querySelectorAll('canvas, svg, .chart, .recharts-wrapper, [class*="chart"]'); const tokenChart = document.querySelector('[class*="token"], [id*="token"], [data-chart*="token"]'); return { hasCharts: charts.length > 0, chartCount: charts.length, hasTokenChart: !!tokenChart, chartTypes: Array.from(charts).map(c => c.tagName.toLowerCase()).slice(0, 5) }; }); if (!result.hasCharts) { return { passed: null, skipped: true, message: 'No charts found on metrics page' }; } return { passed: true, message: `Found ${result.chartCount} charts (types: ${result.chartTypes.join(', ')})` }; }, async searchMetricsChart(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); await delay(1000); const result = await page.evaluate(() => { // Dedicated "Search Activity Over Time" chart canvas and the // "Search Engines" breakdown container rendered by pages/metrics.html. const chartCanvas = document.querySelector('canvas#search-activity-chart'); const engineBreakdown = document.querySelector('#search-engine-breakdown'); return { hasChartCanvas: !!chartCanvas, hasEngineBreakdown: !!engineBreakdown }; }); const passed = result.hasChartCanvas && result.hasEngineBreakdown; return { passed, message: passed ? 'Search metrics section found (#search-activity-chart + #search-engine-breakdown)' : `Search metrics elements missing (chart=${result.hasChartCanvas}, engineBreakdown=${result.hasEngineBreakdown})` }; }, async rateLimitingSection(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { const rateSection = document.querySelector( '[class*="rate-limit"], ' + '[id*="rate-limit"], ' + '.rate-limiting' ); const hasRateData = document.body.textContent?.toLowerCase().includes('rate limit'); return { hasRateSection: !!rateSection, hasRateData }; }); if (!result.hasRateSection && !result.hasRateData) { return { passed: null, skipped: true, message: 'No rate limiting section found' }; } return { passed: true, message: 'Rate limiting section found' }; }, async userSatisfactionSection(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/`); const result = await page.evaluate(() => { // Dedicated "User Satisfaction" breakdown container and the // average user rating metric tile, both rendered by pages/metrics.html. const satisfactionSection = document.querySelector('#rating-breakdown'); const avgRatingValue = document.querySelector('.ldr-metric-value#avg-user-rating'); return { hasSatisfactionSection: !!satisfactionSection, hasAvgRatingValue: !!avgRatingValue }; }); const passed = result.hasSatisfactionSection && result.hasAvgRatingValue; return { passed, message: passed ? 'User satisfaction metrics found (#rating-breakdown + #avg-user-rating)' : `User satisfaction elements missing (section=${result.hasSatisfactionSection}, avgValue=${result.hasAvgRatingValue})` }; } }; // ============================================================================ // Cost Analytics Tests // ============================================================================ const CostAnalyticsTests = { async costAnalyticsPageLoads(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/costs`); const result = await page.evaluate(() => { // Page-specific container rendered server-side by pages/cost_analytics.html: //
//
... const container = document.querySelector('#cost-analytics.ldr-page .ldr-cost-analytics-container'); const header = document.querySelector('#cost-analytics .ldr-cost-header h1'); return { hasContainer: !!container, headerText: header?.textContent?.trim() }; }); const passed = result.hasContainer; return { passed, message: passed ? `Cost analytics page loaded (header: "${result.headerText}")` : 'Cost analytics container (#cost-analytics .ldr-cost-analytics-container) not found — wrong page loaded' }; }, async costBreakdownChart(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/costs`); await delay(1000); const result = await page.evaluate(() => { const charts = document.querySelectorAll('canvas, svg, .chart, .recharts-wrapper'); const breakdownSection = document.querySelector('[class*="breakdown"], [id*="breakdown"]'); return { hasCharts: charts.length > 0, chartCount: charts.length, hasBreakdown: !!breakdownSection }; }); if (!result.hasCharts && !result.hasBreakdown) { return { passed: null, skipped: true, message: 'No cost breakdown visualization found' }; } return { passed: true, message: `Cost breakdown found (charts: ${result.chartCount})` }; }, async pricingTableDisplays(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/costs`); const result = await page.evaluate(() => { const table = document.querySelector('table, .pricing-table, .ldr-table'); const pricingSection = document.querySelector('[class*="pricing"], [id*="pricing"]'); if (table) { const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.toLowerCase().trim()); const hasPriceColumn = headers.some(h => h.includes('price') || h.includes('cost') || h.includes('$')); const hasModelColumn = headers.some(h => h.includes('model') || h.includes('provider')); return { hasTable: true, headerCount: headers.length, hasPriceColumn, hasModelColumn }; } return { hasTable: false, hasPricingSection: !!pricingSection }; }); if (!result.hasTable && !result.hasPricingSection) { return { passed: null, skipped: true, message: 'No pricing table found' }; } return { passed: result.hasTable, message: result.hasTable ? `Pricing table found (price column=${result.hasPriceColumn}, model column=${result.hasModelColumn})` : 'Pricing section found' }; }, async researchCostsTable(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/costs`); const result = await page.evaluate(() => { const tables = document.querySelectorAll('table'); const researchTable = Array.from(tables).find(t => { const text = t.textContent?.toLowerCase() || ''; return text.includes('research') || text.includes('query'); }); if (researchTable) { const rows = researchTable.querySelectorAll('tbody tr'); return { hasTable: true, rowCount: rows.length }; } return { hasTable: false }; }); if (!result.hasTable) { return { passed: null, skipped: true, message: 'No research costs table found' }; } return { passed: true, message: `Research costs table found (${result.rowCount} rows)` }; } }; // ============================================================================ // Star Reviews Tests // ============================================================================ const StarReviewsTests = { async starReviewsPageLoads(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/star-reviews`); const result = await page.evaluate(() => { // Page-specific container rendered server-side by pages/star_reviews.html: //
const container = document.querySelector('.ldr-star-reviews-container'); const header = container?.querySelector('.ldr-metrics-header h1'); return { hasContainer: !!container, headerText: header?.textContent?.trim() }; }); const passed = result.hasContainer; return { passed, message: passed ? `Star reviews page loaded (header: "${result.headerText}")` : 'Star reviews container (.ldr-star-reviews-container) not found — wrong page loaded' }; }, async ratingsDistributionChart(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/star-reviews`); await delay(1000); const result = await page.evaluate(() => { const charts = document.querySelectorAll('canvas, svg, .chart, .recharts-wrapper'); const distribution = document.querySelector('[class*="distribution"], [id*="distribution"]'); // Check for bar chart or histogram const bars = document.querySelectorAll('.bar, rect, [class*="bar"]'); return { hasCharts: charts.length > 0, hasDistribution: !!distribution, hasBars: bars.length > 0, chartCount: charts.length }; }); if (!result.hasCharts && !result.hasDistribution && !result.hasBars) { return { passed: null, skipped: true, message: 'No ratings distribution chart found' }; } return { passed: true, message: `Ratings distribution found (charts: ${result.chartCount})` }; }, async averageRatingDisplay(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/star-reviews`); const result = await page.evaluate(() => { const avgElement = document.querySelector( '[class*="average"], ' + '[id*="average"], ' + '.overall-rating, ' + '.avg-rating' ); // Look for a number that could be an average (e.g., 4.2, 3.5) const avgPattern = /(\d+(?:\.\d*)?)\s*(?:\/\s*5|stars?|out of)/i; const bodyText = document.body.textContent || ''; const avgMatch = bodyText.match(avgPattern); return { hasAvgElement: !!avgElement, avgText: avgElement?.textContent?.trim(), foundAvgInText: !!avgMatch, avgValue: avgMatch ? avgMatch[1] : null }; }); if (!result.hasAvgElement && !result.foundAvgInText) { return { passed: null, skipped: true, message: 'No average rating display found' }; } return { passed: true, message: result.hasAvgElement ? `Average rating displayed: "${result.avgText}"` : `Average rating found in text: ${result.avgValue}` }; } }; // ============================================================================ // Link Analytics Tests // ============================================================================ const LinkAnalyticsTests = { async linkAnalyticsPageLoads(page, baseUrl) { await navigateTo(page, `${baseUrl}/metrics/links`); const result = await page.evaluate(() => { // Page-specific container rendered server-side by pages/link_analytics.html: //