项目文件夹

文件
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 行
17 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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);
});