learningcircuit--local-deep-research
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
768 行
29 KiB
JavaScript
768 行
29 KiB
JavaScript
#!/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:
|
|
// <div class="ldr-page active" id="metrics">
|
|
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:
|
|
// <div class="ldr-page active" id="cost-analytics">
|
|
// <div class="ldr-cost-analytics-container"> ...
|
|
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:
|
|
// <div class="ldr-star-reviews-container">
|
|
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:
|
|
// <div class="ldr-link-analytics-container">
|
|
const container = document.querySelector('.ldr-link-analytics-container');
|
|
const header = container?.querySelector('h1');
|
|
|
|
return {
|
|
hasContainer: !!container,
|
|
headerText: header?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
const passed = result.hasContainer;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Link analytics page loaded (header: "${result.headerText}")`
|
|
: 'Link analytics container (.ldr-link-analytics-container) not found — wrong page loaded'
|
|
};
|
|
},
|
|
|
|
async topDomainsTable(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/links`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const table = document.querySelector('table');
|
|
if (!table) return { hasTable: false };
|
|
|
|
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.toLowerCase().trim());
|
|
const rows = table.querySelectorAll('tbody tr');
|
|
|
|
return {
|
|
hasTable: true,
|
|
hasDomainColumn: headers.some(h => h.includes('domain') || h.includes('site') || h.includes('url')),
|
|
hasCountColumn: headers.some(h => h.includes('count') || h.includes('visits') || h.includes('links')),
|
|
rowCount: rows.length
|
|
};
|
|
});
|
|
|
|
if (!result.hasTable) {
|
|
return { passed: null, skipped: true, message: 'No domains table found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.hasDomainColumn,
|
|
message: `Domains table found (${result.rowCount} rows, domain=${result.hasDomainColumn}, count=${result.hasCountColumn})`
|
|
};
|
|
},
|
|
|
|
async domainClassificationSection(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/links`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const classSection = document.querySelector(
|
|
'[class*="classification"], ' +
|
|
'[id*="classification"], ' +
|
|
'.domain-types'
|
|
);
|
|
|
|
const hasCategories = document.body.textContent?.toLowerCase().includes('academic') ||
|
|
document.body.textContent?.toLowerCase().includes('news') ||
|
|
document.body.textContent?.toLowerCase().includes('government');
|
|
|
|
return {
|
|
hasClassSection: !!classSection,
|
|
hasCategories
|
|
};
|
|
});
|
|
|
|
if (!result.hasClassSection && !result.hasCategories) {
|
|
return { passed: null, skipped: true, message: 'No domain classification section found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: 'Domain classification section found'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Metrics API Tests
|
|
// ============================================================================
|
|
const MetricsApiTests = {
|
|
async metricsApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/metrics/api/metrics`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
hasData: Object.keys(data).length > 0
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Metrics API responds (status ${result.status}, hasData=${result.hasData})`
|
|
: `Metrics API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
},
|
|
|
|
async pricingApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/costs`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/metrics/api/pricing`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
modelCount: Array.isArray(data) ? data.length : Object.keys(data).length
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
if (!result.ok && result.status === 404) {
|
|
return { passed: null, skipped: true, message: 'Pricing API endpoint not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Pricing API responds (${result.modelCount} models)`
|
|
: `Pricing API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
},
|
|
|
|
async starReviewsApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/star-reviews`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/metrics/api/star-reviews`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
hasData: Object.keys(data).length > 0
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
if (!result.ok && result.status === 404) {
|
|
return { passed: null, skipped: true, message: 'Star reviews API endpoint not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Star reviews API responds (hasData=${result.hasData})`
|
|
: `Star reviews API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
},
|
|
|
|
async linkAnalyticsApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/metrics/links`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/metrics/api/link-analytics`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
hasData: Object.keys(data).length > 0
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
if (!result.ok && result.status === 404) {
|
|
return { passed: null, skipped: true, message: 'Link analytics API endpoint not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Link analytics API responds (hasData=${result.hasData})`
|
|
: `Link analytics API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Metrics Dashboard Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Metrics Dashboard Tests');
|
|
const { page } = ctx;
|
|
const { baseUrl } = ctx.config;
|
|
|
|
const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
|
|
async function run(category, name, testFn) {
|
|
try {
|
|
const result = await withTimeout(
|
|
testFn(page, baseUrl),
|
|
subTestTimeout,
|
|
`${category}/${name}`
|
|
);
|
|
if (result.skipped) {
|
|
results.skip(category, name, result.message);
|
|
} else {
|
|
results.add(category, name, result.passed, result.message);
|
|
}
|
|
} catch (error) {
|
|
results.add(category, name, false, `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Metrics Dashboard Tests
|
|
log.section('Metrics Dashboard');
|
|
await run('Dashboard', 'Metrics Dashboard Loads', (p, u) => MetricsDashboardTests.metricsDashboardLoads(p, u));
|
|
await run('Dashboard', 'Metrics Overview Cards', (p, u) => MetricsDashboardTests.metricsOverviewCards(p, u));
|
|
await run('Dashboard', 'Period Filter Dropdown', (p, u) => MetricsDashboardTests.periodFilterDropdown(p, u));
|
|
await run('Dashboard', 'Mode Filter Dropdown', (p, u) => MetricsDashboardTests.modeFilterDropdown(p, u));
|
|
await run('Dashboard', 'Token Usage Chart', (p, u) => MetricsDashboardTests.tokenUsageChart(p, u));
|
|
await run('Dashboard', 'Search Metrics Chart', (p, u) => MetricsDashboardTests.searchMetricsChart(p, u));
|
|
await run('Dashboard', 'Rate Limiting Section', (p, u) => MetricsDashboardTests.rateLimitingSection(p, u));
|
|
await run('Dashboard', 'User Satisfaction Section', (p, u) => MetricsDashboardTests.userSatisfactionSection(p, u));
|
|
|
|
// Cost Analytics Tests
|
|
log.section('Cost Analytics');
|
|
await run('Costs', 'Cost Analytics Page Loads', (p, u) => CostAnalyticsTests.costAnalyticsPageLoads(p, u));
|
|
await run('Costs', 'Cost Breakdown Chart', (p, u) => CostAnalyticsTests.costBreakdownChart(p, u));
|
|
await run('Costs', 'Pricing Table Displays', (p, u) => CostAnalyticsTests.pricingTableDisplays(p, u));
|
|
await run('Costs', 'Research Costs Table', (p, u) => CostAnalyticsTests.researchCostsTable(p, u));
|
|
|
|
// Star Reviews Tests
|
|
log.section('Star Reviews');
|
|
await run('Reviews', 'Star Reviews Page Loads', (p, u) => StarReviewsTests.starReviewsPageLoads(p, u));
|
|
await run('Reviews', 'Ratings Distribution Chart', (p, u) => StarReviewsTests.ratingsDistributionChart(p, u));
|
|
await run('Reviews', 'Average Rating Display', (p, u) => StarReviewsTests.averageRatingDisplay(p, u));
|
|
|
|
// Link Analytics Tests
|
|
log.section('Link Analytics');
|
|
await run('Links', 'Link Analytics Page Loads', (p, u) => LinkAnalyticsTests.linkAnalyticsPageLoads(p, u));
|
|
await run('Links', 'Top Domains Table', (p, u) => LinkAnalyticsTests.topDomainsTable(p, u));
|
|
await run('Links', 'Domain Classification Section', (p, u) => LinkAnalyticsTests.domainClassificationSection(p, u));
|
|
|
|
// Metrics API Tests
|
|
log.section('Metrics APIs');
|
|
await run('API', 'Metrics API Responds', (p, u) => MetricsApiTests.metricsApiResponds(p, u));
|
|
await run('API', 'Pricing API Responds', (p, u) => MetricsApiTests.pricingApiResponds(p, u));
|
|
await run('API', 'Star Reviews API Responds', (p, u) => MetricsApiTests.starReviewsApiResponds(p, u));
|
|
await run('API', 'Link Analytics API Responds', (p, u) => MetricsApiTests.linkAnalyticsApiResponds(p, u));
|
|
|
|
} catch (error) {
|
|
log.error(`Fatal error: ${error.message}`);
|
|
console.error(error.stack);
|
|
} finally {
|
|
results.print();
|
|
results.save();
|
|
await teardownTest(ctx);
|
|
process.exit(results.exitCode());
|
|
}
|
|
}
|
|
|
|
// Run if executed directly
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error('Test runner failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { MetricsDashboardTests, CostAnalyticsTests, StarReviewsTests, LinkAnalyticsTests, MetricsApiTests };
|