/** * Export Dropdown UI Tests * * Tests for the export dropdown functionality on the results page. * Verifies that: * 1. PDF button is separate from dropdown * 2. Export dropdown opens and shows all formats * 3. All expected export options are present (Markdown, LaTeX, Quarto, ODT, RIS) */ const puppeteer = require('puppeteer'); const AuthHelper = require('./auth_helper'); const { getPuppeteerLaunchOptions } = require('./puppeteer_config'); const BASE_URL = 'http://127.0.0.1:5000'; const colors = { reset: '\x1b[0m', 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 testExportDropdown() { const browser = await puppeteer.launch(getPuppeteerLaunchOptions()); const page = await browser.newPage(); // Track test results const results = { passed: 0, failed: 0, tests: [] }; function recordTest(name, passed, details = '') { results.tests.push({ name, passed, details }); if (passed) { results.passed++; log(`āœ… ${name}`, 'success'); } else { results.failed++; log(`āŒ ${name}: ${details}`, 'error'); } } try { // Authenticate const authHelper = new AuthHelper(page, BASE_URL); log('šŸ” Authenticating...', 'info'); await authHelper.ensureAuthenticated(); log('āœ… Authenticated', 'success'); // Navigate to history to find a completed research log('\n=== FINDING COMPLETED RESEARCH ===', 'section'); await page.goto(`${BASE_URL}/history`, { waitUntil: 'domcontentloaded' }); await delay(2000); // Look for a completed research item const completedResearch = await page.evaluate(() => { const items = document.querySelectorAll('.ldr-history-item, [data-research-id]'); for (const item of items) { const status = item.querySelector('.ldr-status-badge, .status-badge'); if (status && status.textContent.toLowerCase().includes('completed')) { const link = item.querySelector('a[href*="/results/"]'); return link ? link.href : null; } } // If no completed research, return the first research link const firstLink = document.querySelector('a[href*="/results/"]'); return firstLink ? firstLink.href : null; }); if (!completedResearch) { log('āš ļø No completed research found, creating mock results page test', 'warning'); // We can still test the HTML structure by navigating to a fake results URL // and checking if the export elements exist in the template await page.goto(`${BASE_URL}/results/test-123`, { waitUntil: 'domcontentloaded' }); } else { log(`šŸ“ Navigating to: ${completedResearch}`, 'info'); await page.goto(completedResearch, { waitUntil: 'domcontentloaded' }); } await delay(2000); log('\n=== TESTING EXPORT UI STRUCTURE ===', 'section'); // Test 1: PDF button exists and is separate from dropdown const pdfButtonExists = await page.evaluate(() => { const pdfBtn = document.getElementById('download-pdf-btn'); if (!pdfBtn) return { exists: false }; // Check it's not inside a dropdown const isInDropdown = pdfBtn.closest('.dropdown-menu') !== null; return { exists: true, isInDropdown, text: pdfBtn.textContent.trim(), hasIcon: !!pdfBtn.querySelector('i.fa-file-pdf') }; }); recordTest( 'PDF button exists and is separate from dropdown', pdfButtonExists.exists && !pdfButtonExists.isInDropdown, pdfButtonExists.exists ? (pdfButtonExists.isInDropdown ? 'PDF button is inside dropdown' : '') : 'PDF button not found' ); // Test 2: Export dropdown button exists const exportDropdownExists = await page.evaluate(() => { const dropdownBtn = document.getElementById('export-dropdown-btn'); if (!dropdownBtn) return { exists: false }; return { exists: true, text: dropdownBtn.textContent.trim(), hasToggle: dropdownBtn.classList.contains('dropdown-toggle'), hasIcon: !!dropdownBtn.querySelector('i.fa-download') }; }); recordTest( 'Export dropdown button exists', exportDropdownExists.exists && exportDropdownExists.hasToggle, exportDropdownExists.exists ? '' : 'Export dropdown button not found' ); // Test 3: Click dropdown and verify it opens if (exportDropdownExists.exists) { await page.click('#export-dropdown-btn'); await delay(500); const dropdownOpened = await page.evaluate(() => { const menu = document.querySelector('#export-dropdown-btn + .dropdown-menu, .dropdown-menu[aria-labelledby="export-dropdown-btn"]'); if (!menu) return { opened: false, reason: 'Menu not found' }; const isVisible = menu.classList.contains('show') || getComputedStyle(menu).display !== 'none'; return { opened: isVisible, reason: isVisible ? '' : 'Menu not visible' }; }); recordTest( 'Export dropdown opens on click', dropdownOpened.opened, dropdownOpened.reason ); } // Test 4: Check all expected export options exist const expectedFormats = [ { id: 'export-markdown-btn', name: 'Markdown', icon: 'fa-file-alt' }, { id: 'export-latex-btn', name: 'LaTeX', icon: 'fa-file-code' }, { id: 'export-quarto-btn', name: 'Quarto', icon: 'fa-file-code' }, { id: 'export-odt-btn', name: 'ODT (LibreOffice)', icon: 'fa-file-word' }, { id: 'export-ris-btn', name: 'RIS (Zotero)', icon: 'fa-file-export' } ]; for (const format of expectedFormats) { const formatExists = await page.evaluate((formatId) => { const btn = document.getElementById(formatId); if (!btn) return { exists: false }; return { exists: true, text: btn.textContent.trim(), isInDropdown: btn.closest('.dropdown-menu') !== null, isClickable: btn.tagName === 'A' || btn.tagName === 'BUTTON' }; }, format.id); recordTest( `${format.name} export option exists in dropdown`, formatExists.exists && formatExists.isInDropdown, formatExists.exists ? (formatExists.isInDropdown ? '' : 'Not in dropdown menu') : `${format.id} not found` ); } // Test 5: Verify dropdown has proper structure (dividers) const dropdownStructure = await page.evaluate(() => { const menu = document.querySelector('.dropdown-menu[aria-labelledby="export-dropdown-btn"]'); if (!menu) return { valid: false, reason: 'Menu not found' }; const dividers = menu.querySelectorAll('.dropdown-divider, hr.dropdown-divider'); const items = menu.querySelectorAll('.dropdown-item'); return { valid: true, itemCount: items.length, dividerCount: dividers.length, hasProperStructure: items.length >= 5 && dividers.length >= 2 }; }); recordTest( 'Dropdown has proper structure with dividers', dropdownStructure.valid && dropdownStructure.hasProperStructure, dropdownStructure.valid ? `${dropdownStructure.itemCount} items, ${dropdownStructure.dividerCount} dividers` : dropdownStructure.reason ); // Test 6: Close dropdown by clicking elsewhere await page.click('body'); await delay(300); const dropdownClosed = await page.evaluate(() => { const menu = document.querySelector('.dropdown-menu[aria-labelledby="export-dropdown-btn"]'); if (!menu) return { closed: true }; const isHidden = !menu.classList.contains('show') && getComputedStyle(menu).display === 'none'; return { closed: isHidden }; }); recordTest( 'Dropdown closes when clicking elsewhere', dropdownClosed.closed, dropdownClosed.closed ? '' : 'Dropdown still visible' ); // Print summary log('\n=== TEST SUMMARY ===', 'section'); log(`Total: ${results.passed + results.failed} tests`, 'info'); log(`Passed: ${results.passed}`, 'success'); if (results.failed > 0) { log(`Failed: ${results.failed}`, 'error'); } if (results.failed > 0) { throw new Error(`${results.failed} test(s) failed`); } log('\nāœ… All export dropdown tests passed!', 'success'); } catch (error) { log(`\nāŒ Test failed: ${error.message}`, 'error'); throw error; } finally { await browser.close(); } } // Run tests testExportDropdown().catch(error => { console.error('Test execution failed:', error); process.exit(1); });