/** * 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 };