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
451 lines
17 KiB
JavaScript
451 lines
17 KiB
JavaScript
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);
|
||
});
|