const puppeteer = require('puppeteer'); const { getPuppeteerLaunchOptions } = require('./puppeteer_config'); const AuthHelper = require('./auth_helper'); // NAVIGATION NOTE: Using 'domcontentloaded' instead of 'networkidle2' for page.goto() // because networkidle2 waits for no network activity for 500ms, but WebSocket // connections and background polling keep the network active, causing infinite hangs. // See: test_login_validation.js and auth_helper.js for detailed explanation. // Test configuration const BASE_URL = 'http://127.0.0.1:5000'; // Colors for console output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', 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 startResearch(page, query) { log(`šŸ”¬ Starting research: "${query}"`, 'info'); await page.goto(`${BASE_URL}/`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#query', { timeout: 10000 }); // Use longer iterations for cancellation test await page.evaluate(() => { // Try to set iterations to a higher value if the field exists const iterationsInput = document.querySelector('input[name="iterations"], #iterations'); if (iterationsInput) { iterationsInput.value = '3'; } }); // Set query await page.evaluate((q) => { document.getElementById('query').value = q; }, query); // Submit form and wait for potential navigation or AJAX response const submitButton = await page.$('button[type="submit"], #submit-research, #start-research-btn'); if (!submitButton) { log('āŒ Submit button not found', 'error'); return false; } // Click submit and wait for navigation to research page try { await Promise.all([ page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }), submitButton.click() ]); } catch (e) { // Navigation might timeout but research could still have started log(`Navigation wait: ${e.message}`, 'info'); } // The cancellation flow needs the research/progress page; wait for either the // URL to land or any progress UI to appear instead of a fixed sleep. await page.waitForFunction( () => { const p = window.location.pathname; return p.includes('/research/') || p.includes('/progress') || !!document.querySelector('.cancel-btn, .stop-btn, .progress-info, .progress-text'); }, { timeout: 5000 } ).catch(() => {}); // Check if we're on a research page (URL-based check is safest during navigation) const url = page.url(); if (url.includes('/research/') || url.includes('/progress')) { log('āœ… Research started (page navigated)', 'success'); return true; } return false; } async function testCancellation() { const browser = await puppeteer.launch(getPuppeteerLaunchOptions()); const page = await browser.newPage(); // Set console log handler page.on('console', msg => { if (msg.type() === 'error' && !msg.text().includes('favicon')) { log(`Browser console error: ${msg.text()}`, 'error'); } }); try { // Register user using AuthHelper for consistent behavior const auth = new AuthHelper(page, BASE_URL); await auth.ensureAuthenticated(); // Navigate to home page after authentication await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 10000 }); // Test 1: Cancel during early stage log('\n=== TEST 1: EARLY CANCELLATION ===', 'section'); await startResearch(page, 'Complex analysis of quantum computing applications'); // The cancel button only renders once the research starts; the waitForSelector // below is the real signal we need, so no settle delay is required here. // Find and click cancel button - use waitForSelector to ensure page is stable let cancelButton = null; try { cancelButton = await page.waitForSelector('.cancel-btn, .stop-btn, button[data-action="cancel"]', { timeout: 5000 }); } catch { log('āš ļø No cancel button found (may not be visible yet)', 'warning'); } if (cancelButton) { log('šŸ›‘ Clicking cancel button...', 'info'); // Handle potential confirmation dialog page.once('dialog', async dialog => { log(`šŸ“‹ Confirmation dialog: ${dialog.message()}`, 'info'); await dialog.accept(); }); await cancelButton.click(); // Poll for the server-acknowledged cancel state instead of a fixed sleep; // .catch(() => {}) keeps behavior identical when the page navigates away. await page.waitForFunction( () => { const status = document.querySelector('.status, .research-status')?.textContent || ''; return /cancel|stop|suspend/i.test(status); }, { timeout: 5000 } ).catch(() => {}); // Check if research was cancelled - wrap in try-catch for navigation safety try { const cancelStatus = await page.evaluate(() => { const statusElement = document.querySelector('.status, .research-status'); const progressText = document.querySelector('.progress-info, .progress-text'); return { status: statusElement?.textContent || '', progress: progressText?.textContent || '', hasCancelButton: !!document.querySelector('.cancel-btn, .stop-btn') }; }); log(`šŸ“Š Status after cancellation:`, 'info'); log(` - Status: ${cancelStatus.status}`, 'info'); log(` - Progress: ${cancelStatus.progress}`, 'info'); if (cancelStatus.status.toLowerCase().includes('cancel') || cancelStatus.status.toLowerCase().includes('stopped') || cancelStatus.status.toLowerCase().includes('suspended')) { log('āœ… Research cancelled successfully', 'success'); } } catch { // Page may have navigated after cancellation - that's OK log('āœ… Cancellation triggered (page state changed)', 'success'); } } else { log('āš ļø No cancel button found', 'warning'); } // Test 2: Cancel during mid-stage log('\n=== TEST 2: MID-STAGE CANCELLATION ===', 'section'); await startResearch(page, 'Comprehensive study of artificial intelligence ethics'); // Wait for research to progress log('ā³ Waiting for research to progress...', 'info'); await delay(10000); // Wait 10 seconds // Get progress before cancellation - wrap in try-catch let progressBefore = { percentage: '0%', text: '' }; try { progressBefore = await page.evaluate(() => { const progressBar = document.querySelector('.progress-bar, [role="progressbar"]'); const progressText = document.querySelector('.progress-info, .progress-text'); return { percentage: progressBar?.getAttribute('aria-valuenow') || progressBar?.style.width || progressText?.textContent || '0%', text: progressText?.textContent || '' }; }); } catch { log('āš ļø Could not get progress (page navigating)', 'warning'); } log(`šŸ“Š Progress before cancellation: ${progressBefore.percentage}`, 'info'); // Cancel research let cancelButton2 = null; try { cancelButton2 = await page.waitForSelector('.cancel-btn, .stop-btn, button[data-action="cancel"]', { timeout: 5000 }); } catch { log('āš ļø No cancel button found for mid-stage test', 'warning'); } if (cancelButton2) { page.once('dialog', async dialog => { await dialog.accept(); }); await cancelButton2.click(); // Same pattern as the early-cancel branch: wait for the cancel state to // surface in the UI rather than sleeping for a fixed interval. await page.waitForFunction( () => { const status = document.querySelector('.status-badge, .research-status')?.textContent || ''; return /cancel|stop|suspend/i.test(status); }, { timeout: 5000 } ).catch(() => {}); try { const afterCancel = await page.evaluate(() => { const progressText = document.querySelector('.progress-info, .progress-text'); const statusBadge = document.querySelector('.status-badge, .research-status'); return { progress: progressText?.textContent || '', status: statusBadge?.textContent || '' }; }); log(`šŸ“Š After mid-stage cancellation:`, 'info'); log(` - Progress: ${afterCancel.progress}`, 'info'); log(` - Status: ${afterCancel.status}`, 'info'); } catch { log('āœ… Cancellation triggered (page state changed)', 'success'); } log('āœ… Mid-stage cancellation tested', 'success'); } // Test 3: Check cancelled research in history log('\n=== TEST 3: CANCELLED RESEARCH IN HISTORY ===', 'section'); await page.goto(`${BASE_URL}/history`, { waitUntil: 'domcontentloaded' }); try { await page.waitForSelector('.ldr-history-container, .ldr-history-list, .history-item', { timeout: 10000 }); const cancelledItems = await page.evaluate(() => { const items = []; const historyItems = document.querySelectorAll('.history-item'); historyItems.forEach(item => { const status = item.querySelector('.status-badge, .status')?.textContent || ''; const query = item.querySelector('.query-text, .research-query')?.textContent || ''; if (status.toLowerCase().includes('cancel') || status.toLowerCase().includes('suspended') || status.toLowerCase().includes('stopped')) { items.push({ query: query.trim(), status: status.trim() }); } }); return items; }); if (cancelledItems.length > 0) { log(`āœ… Found ${cancelledItems.length} cancelled research items in history:`, 'success'); cancelledItems.forEach(item => { log(` - "${item.query}" - Status: ${item.status}`, 'info'); }); } else { log('āš ļø No cancelled items found in history (research may have completed too fast)', 'warning'); } } catch { log('āš ļø Could not check history (page issue)', 'warning'); } // Test 4: Attempt to resume cancelled research (if feature exists) log('\n=== TEST 4: RESUME FUNCTIONALITY ===', 'section'); try { const resumeButtons = await page.$$('.resume-btn, button[data-action="resume"]'); if (resumeButtons.length > 0) { log('šŸ“Š Resume functionality available', 'info'); // Click first resume button await resumeButtons[0].click(); // Wait for the resume to actually route to a research/progress page // instead of using a fixed sleep. await page.waitForFunction( () => { const p = window.location.pathname; return p.includes('/research') || p.includes('/progress'); }, { timeout: 5000 } ).catch(() => {}); // Check if research resumed const url = page.url(); if (url.includes('/research') || url.includes('/progress')) { log('āœ… Resume functionality works', 'success'); } } else { log('ā„¹ļø No resume functionality found (might not be implemented)', 'info'); } } catch { log('ā„¹ļø Resume test skipped (page issue)', 'info'); } // Test 5: Multiple cancellations log('\n=== TEST 5: RAPID CANCELLATION ===', 'section'); await startResearch(page, 'Quick test for immediate cancellation'); // The waitForSelector below already waits for the cancel button to render; // no extra settle delay is needed. try { const quickCancel = await page.waitForSelector('.cancel-btn, .stop-btn', { timeout: 3000 }); if (quickCancel) { page.once('dialog', async dialog => { await dialog.accept(); }); await quickCancel.click(); log('āœ… Immediate cancellation tested', 'success'); } } catch { log('āš ļø Quick cancel button not found (research may have completed)', 'warning'); } log('\nāœ… Research cancellation test completed successfully!', 'success'); } catch (error) { log(`\nāŒ Test failed: ${error.message}`, 'error'); throw error; } finally { await browser.close(); } } // Run the test testCancellation().catch(error => { console.error('Test execution failed:', error); process.exit(1); });