Files
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

373 lines
14 KiB
JavaScript

/**
* Browser-based UI tests using Puppeteer
* Tests the actual browser rendering and JavaScript execution of pages
*/
const puppeteer = require('puppeteer');
const AuthHelper = require('./auth_helper');
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
const fs = require('fs');
const path = require('path');
const DEFAULT_TIMEOUT = 10000; // Increased for pages with many network requests
const DEFAULT_WAIT = 3000; // More time for JS to execute
class BrowserTester {
constructor(baseUrl = 'http://127.0.0.1:5000') {
this.baseUrl = baseUrl;
this.browser = null;
this.page = null;
}
async setup() {
console.log('🚀 Starting browser test session...');
this.browser = await puppeteer.launch(getPuppeteerLaunchOptions());
this.page = await this.browser.newPage();
this.authHelper = new AuthHelper(this.page, this.baseUrl);
// Listen to console errors
this.page.on('console', msg => {
if (msg.type() === 'error') {
console.log(` Browser error: ${msg.text()}`);
}
});
// Listen to JavaScript errors
this.page.on('pageerror', error => {
console.log(`❌ [JS ERROR] ${error.message}`);
});
// Listen to failed requests
this.page.on('requestfailed', request => {
const url = request.url();
const failure = request.failure();
const error = failure ? failure.errorText : 'Unknown error';
// Only log for important resources, ignore favicon etc.
if (!url.includes('favicon') && !url.includes('.ico')) {
console.log(`🔴 [REQUEST FAILED] ${url} - ${error}`);
}
});
// Listen to 404 responses to identify missing resources
this.page.on('response', response => {
if (response.status() === 404) {
const url = response.url();
if (!url.includes('favicon') && !url.includes('.ico')) {
console.log(` [404 RESPONSE] ${url}`);
}
}
});
// Ensure authentication before running tests
console.log('\n🔐 Ensuring authentication...');
try {
await this.authHelper.ensureAuthenticatedWithTimeout();
} catch (error) {
console.error('❌ Authentication failed:', error.message);
throw error;
}
}
async teardown() {
if (this.browser) {
await this.browser.close();
console.log('🏁 Browser test session ended');
}
}
async testPage(urlPath, testName, customTests = null) {
const url = `${this.baseUrl}${urlPath}`;
console.log(`\n📄 Testing ${testName}: ${url}`);
try {
// Skip navigation if already on the target page (avoids ERR_ABORTED from double-navigation)
const currentPath = new URL(this.page.url()).pathname;
const targetPath = new URL(url).pathname;
if (currentPath !== targetPath) {
await this.page.goto(url, {
waitUntil: 'domcontentloaded', // Don't wait for all network requests
timeout: DEFAULT_TIMEOUT
});
} else {
console.log(` Already on ${testName}, skipping navigation`);
}
console.log(`✅ ${testName} loaded successfully`);
// Wait for JavaScript to execute
await new Promise(resolve => setTimeout(resolve, DEFAULT_WAIT));
// Basic checks for all pages
const basicChecks = await this.page.evaluate(() => {
return {
hasTitle: document.title.length > 0,
hasBody: document.body !== null,
hasNoJSErrors: !window.hasJavaScriptErrors, // This would need to be set by error handlers
bodyVisible: document.body && window.getComputedStyle(document.body).display !== 'none'
};
});
console.log(`🔍 Basic checks for ${testName}:`);
console.log(` Has title: ${basicChecks.hasTitle}`);
console.log(` Has body: ${basicChecks.hasBody}`);
console.log(` Body visible: ${basicChecks.bodyVisible}`);
// Run custom tests if provided and collect failures
const failures = [];
if (customTests) {
const customFailures = await customTests(this.page);
if (Array.isArray(customFailures)) {
failures.push(...customFailures);
}
}
if (failures.length > 0) {
return { success: false, error: failures.join('; ') };
}
return { success: true, checks: basicChecks };
} catch (error) {
console.log(`❌ Error testing ${testName}: ${error.message}`);
return { success: false, error: error.message };
}
}
}
// Custom test functions for specific pages
// Each returns an array of failure strings (empty = all passed)
const metricsPageTests = async (page) => {
console.log('🧪 Running metrics-specific tests...');
const failures = [];
// Check if metrics elements are present
const metricsChecks = await page.evaluate(() => {
const loading = document.getElementById('loading');
const content = document.getElementById('metrics-content');
const error = document.getElementById('error');
const totalTokens = document.getElementById('total-tokens');
const totalResearches = document.getElementById('total-researches');
return {
hasLoadingElement: !!loading,
hasContentElement: !!content,
hasErrorElement: !!error,
hasTotalTokens: !!totalTokens,
hasTotalResearches: !!totalResearches,
loadingVisible: loading ? window.getComputedStyle(loading).display !== 'none' : false,
contentVisible: content ? window.getComputedStyle(content).display !== 'none' : false,
errorVisible: error ? window.getComputedStyle(error).display !== 'none' : false,
tokenValue: totalTokens ? totalTokens.textContent : 'NOT FOUND',
researchValue: totalResearches ? totalResearches.textContent : 'NOT FOUND'
};
});
console.log('📊 Metrics page checks:');
console.log(` Loading visible: ${metricsChecks.loadingVisible}`);
console.log(` Content visible: ${metricsChecks.contentVisible}`);
console.log(` Error visible: ${metricsChecks.errorVisible}`);
console.log(` Total tokens: ${metricsChecks.tokenValue}`);
console.log(` Total researches: ${metricsChecks.researchValue}`);
// Either content or error element must exist (error state is valid in CI with no data)
if (!metricsChecks.hasContentElement && !metricsChecks.hasErrorElement) {
failures.push('Missing both #metrics-content and #error elements');
}
if (!metricsChecks.hasTotalTokens) failures.push('Missing #total-tokens element');
if (!metricsChecks.hasTotalResearches) failures.push('Missing #total-researches element');
// Take screenshot for debugging (skip in CI — diagnostic only)
if (!process.env.CI) {
try {
const screenshotPath = path.join(__dirname, 'screenshots', 'metrics-test.png');
await page.screenshot({ path: screenshotPath });
console.log(`📸 Screenshot saved: ${screenshotPath}`);
} catch (err) {
console.log('⚠️ Could not save screenshot:', err.message);
}
}
return failures;
};
const researchPageTests = async (page) => {
console.log('🧪 Running research page tests...');
const failures = [];
const researchChecks = await page.evaluate(() => {
const queryInput = document.getElementById('query');
const submitButton = document.querySelector('button[type="submit"]');
const modeSelection = document.querySelector('.ldr-mode-selection');
const modeRadios = document.querySelectorAll('input[name="research_mode"]');
return {
hasQueryInput: !!queryInput,
hasSubmitButton: !!submitButton,
hasModeSelection: !!modeSelection,
modeRadioCount: modeRadios.length,
queryInputEnabled: queryInput ? !queryInput.disabled : false,
submitButtonEnabled: submitButton ? !submitButton.disabled : false
};
});
console.log('🔍 Research page checks:');
console.log(` Has query input: ${researchChecks.hasQueryInput}`);
console.log(` Has submit button: ${researchChecks.hasSubmitButton}`);
console.log(` Has mode selection: ${researchChecks.hasModeSelection}`);
console.log(` Mode radio count: ${researchChecks.modeRadioCount}`);
console.log(` Query input enabled: ${researchChecks.queryInputEnabled}`);
console.log(` Submit button enabled: ${researchChecks.submitButtonEnabled}`);
if (!researchChecks.hasQueryInput) failures.push('Missing query input (#query)');
if (!researchChecks.hasSubmitButton) failures.push('Missing submit button');
if (!researchChecks.hasModeSelection) failures.push('Missing mode selection (.ldr-mode-selection)');
// If --search flag is provided, run a test search
if (process.argv.includes('--search') && researchChecks.hasQueryInput && researchChecks.hasSubmitButton) {
console.log('\n🔎 Running test search...');
await page.type('#query', 'Test search query for UI testing');
console.log('✅ Entered test query');
}
return failures;
};
const historyPageTests = async (page) => {
console.log('🧪 Running history page tests...');
const failures = [];
const historyChecks = await page.evaluate(() => {
const historyContainer = document.getElementById('history-container') ||
document.querySelector('.ldr-history-list') ||
document.querySelector('[data-testid="history"]');
const searchInput = document.getElementById('history-search');
return {
hasHistoryContainer: !!historyContainer,
hasSearchInput: !!searchInput,
historyContainerVisible: historyContainer ? window.getComputedStyle(historyContainer).display !== 'none' : false
};
});
console.log('📜 History page checks:');
console.log(` Has history container: ${historyChecks.hasHistoryContainer}`);
console.log(` Has search input: ${historyChecks.hasSearchInput}`);
console.log(` History container visible: ${historyChecks.historyContainerVisible}`);
if (!historyChecks.hasHistoryContainer) failures.push('Missing history container');
if (!historyChecks.hasSearchInput) failures.push('Missing search input (#history-search)');
return failures;
};
const settingsPageTests = async (page) => {
console.log('🧪 Running settings page tests...');
const failures = [];
const settingsChecks = await page.evaluate(() => {
const forms = document.querySelectorAll('form');
const inputs = document.querySelectorAll('input, select, textarea');
const saveButtons = document.querySelectorAll('button[type="submit"]');
// Also look for buttons with Save text
const allButtons = document.querySelectorAll('button');
const saveTextButtons = Array.from(allButtons).filter(btn =>
btn.textContent.toLowerCase().includes('save')
);
return {
hasForm: forms.length > 0,
hasInputs: inputs.length > 0,
hasSaveButtons: saveButtons.length > 0 || saveTextButtons.length > 0,
inputCount: inputs.length,
formCount: forms.length
};
});
console.log('⚙️ Settings page checks:');
console.log(` Has forms: ${settingsChecks.hasForm} (${settingsChecks.formCount} forms)`);
console.log(` Has inputs: ${settingsChecks.hasInputs} (${settingsChecks.inputCount} inputs)`);
console.log(` Has save buttons: ${settingsChecks.hasSaveButtons}`);
if (!settingsChecks.hasInputs) failures.push('Missing input elements');
if (!settingsChecks.hasSaveButtons) failures.push('Missing save buttons');
return failures;
};
// Main test runner
async function runAllTests() {
const tester = new BrowserTester();
try {
await tester.setup();
} catch (error) {
console.error('❌ Failed to setup browser:', error.message);
process.exit(1);
}
// Ensure screenshots directory exists
const screenshotsDir = path.join(__dirname, 'screenshots');
fs.mkdirSync(screenshotsDir, { recursive: true });
const results = [];
// Test main pages
const testCases = [
{ path: '/', name: 'Home/Research Page', tests: researchPageTests },
{ path: '/metrics/', name: 'Metrics Dashboard', tests: metricsPageTests },
{ path: '/history/', name: 'History Page', tests: historyPageTests },
{ path: '/settings/', name: 'Settings Page', tests: settingsPageTests }
];
for (const testCase of testCases) {
const result = await tester.testPage(testCase.path, testCase.name, testCase.tests);
results.push({ ...testCase, result });
}
await tester.teardown();
// Print summary
console.log('\n' + '='.repeat(50));
console.log('📋 TEST SUMMARY');
console.log('='.repeat(50));
let passCount = 0;
let failCount = 0;
results.forEach(({ name, result }) => {
const status = result.success ? '✅ PASS' : '❌ FAIL';
console.log(`${status} ${name}`);
if (!result.success) {
console.log(` Error: ${result.error}`);
failCount++;
} else {
passCount++;
}
});
console.log('\n' + '-'.repeat(30));
console.log(`Total: ${results.length} tests`);
console.log(`Passed: ${passCount}`);
console.log(`Failed: ${failCount}`);
if (failCount === 0) {
console.log('🎉 All tests passed!');
process.exit(0);
} else {
console.log('💥 Some tests failed!');
process.exit(1);
}
}
// Run tests if this file is executed directly
if (require.main === module) {
runAllTests().catch(error => {
console.error('💥 Test runner crashed:', error);
process.exit(1);
});
}
module.exports = { BrowserTester, runAllTests };