Files
learningcircuit--local-deep…/tests/ui_tests/test_metrics_dashboard.js
T
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

451 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});