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
373 lines
14 KiB
JavaScript
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 };
|