#!/usr/bin/env node /** * Real-time Progress UI Tests * * Tests for the research progress page and real-time update elements. * * Run: node test_realtime_progress_ci.js */ const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib'); // ============================================================================ // Progress Page Structure Tests // ============================================================================ const ProgressPageTests = { async progressPageStructure(page, baseUrl) { // First check if there's any research in progress or history await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { // Try to find any research ID from history const item = document.querySelector('[data-research-id], [data-id], a[href*="/progress/"], a[href*="/results/"]'); if (!item) return null; const href = item.href || ''; const match = href.match(/\/(progress|results)\/([a-zA-Z0-9-]+)/); if (match) return match[2]; return item.dataset?.researchId || item.dataset?.id; }); if (!researchId) { // Navigate to a test progress page anyway to check structure await navigateTo(page, `${baseUrl}/progress/test-id`); } else { await navigateTo(page, `${baseUrl}/progress/${researchId}`); } const result = await page.evaluate(() => { // Page-specific container that ONLY the progress template renders // (pages/progress.html:
). // This fails if a login/error page loaded instead of the progress page. const pageContainer = document.querySelector('#research-progress.ldr-page'); // Real progress signals rendered server-side by the progress template, // not generic page chrome: // - the progress bar:
// - the percent display:
// - the status indicator:
const progressBar = document.querySelector('.ldr-progress-bar[role="progressbar"]'); const percentDisplay = document.querySelector('#progress-percentage'); const statusIndicator = document.querySelector('#status-text'); return { hasPageContainer: !!pageContainer, hasProgressBar: !!progressBar, hasPercentDisplay: !!percentDisplay, hasStatusIndicator: !!statusIndicator, is404: document.body.textContent?.toLowerCase().includes('not found') || document.body.textContent?.toLowerCase().includes('404') }; }); if (result.is404) { return { passed: null, skipped: true, message: 'No active research to test progress page' }; } // Require the progress-specific container AND at least one real progress // signal (bar / percent / status) — a bare

on any page is no longer enough. const hasProgressSignal = result.hasProgressBar || result.hasPercentDisplay || result.hasStatusIndicator; const passed = result.hasPageContainer && hasProgressSignal; return { passed, message: passed ? `Progress page structure OK (container #research-progress + ` + `bar=${result.hasProgressBar}, percent=${result.hasPercentDisplay}, status=${result.hasStatusIndicator})` : `Progress page missing expected elements ` + `(container=${result.hasPageContainer}, bar=${result.hasProgressBar}, ` + `percent=${result.hasPercentDisplay}, status=${result.hasStatusIndicator})` }; }, async progressBarExists(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('[data-research-id], [data-id], a[href*="/progress/"]'); if (!item) return null; const href = item.href || ''; const match = href.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : (item.dataset?.researchId || item.dataset?.id); }); if (!researchId) { await navigateTo(page, `${baseUrl}/progress/test-id`); } else { await navigateTo(page, `${baseUrl}/progress/${researchId}`); } const result = await page.evaluate(() => { const progressBar = document.querySelector( '.progress-bar, ' + 'progress, ' + '[role="progressbar"], ' + '.ldr-progress-bar, ' + '[class*="progress-bar"]' ); if (progressBar) { return { exists: true, hasValue: progressBar.value !== undefined || progressBar.style.width || progressBar.getAttribute('aria-valuenow'), value: progressBar.value || progressBar.style.width || progressBar.getAttribute('aria-valuenow') }; } return { exists: false }; }); if (!result.exists) { return { passed: null, skipped: true, message: 'No progress bar element found' }; } return { passed: true, message: `Progress bar exists (value: ${result.value || 'not set'})` }; }, async progressPercentageDisplay(page, baseUrl) { await navigateTo(page, `${baseUrl}/`); // Check home page for any in-progress indicators first const homeResult = await page.evaluate(() => { const percentPattern = /(\d+)\s*%/; const bodyText = document.body.textContent || ''; const match = bodyText.match(percentPattern); const percentElement = document.querySelector( '.percentage, ' + '.progress-percentage, ' + '[class*="percent"]' ); return { hasPercentElement: !!percentElement, percentText: percentElement?.textContent?.trim(), foundInText: !!match, percentValue: match ? match[1] : null }; }); if (homeResult.hasPercentElement || homeResult.foundInText) { return { passed: true, message: `Progress percentage found: ${homeResult.percentText || homeResult.percentValue + '%'}` }; } // Try progress page await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : null; }); if (researchId) { await navigateTo(page, `${baseUrl}/progress/${researchId}`); const progressResult = await page.evaluate(() => { const percentPattern = /(\d+)\s*%/; const bodyText = document.body.textContent || ''; const match = bodyText.match(percentPattern); return { found: !!match, value: match ? match[1] : null }; }); if (progressResult.found) { return { passed: true, message: `Progress percentage displayed: ${progressResult.value}%` }; } } return { passed: null, skipped: true, message: 'No progress percentage display found' }; }, async progressStatusMessages(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('[data-research-id], a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : (item?.dataset?.researchId || null); }); if (!researchId) { return { passed: null, skipped: true, message: 'No research to test status messages' }; } await navigateTo(page, `${baseUrl}/progress/${researchId}`); const result = await page.evaluate(() => { const statusArea = document.querySelector( '.status-messages, ' + '.progress-status, ' + '.status-text, ' + '[class*="status-message"], ' + '.current-step' ); const hasStatusText = document.body.textContent?.toLowerCase().includes('searching') || document.body.textContent?.toLowerCase().includes('analyzing') || document.body.textContent?.toLowerCase().includes('generating') || document.body.textContent?.toLowerCase().includes('completed'); return { hasStatusArea: !!statusArea, statusText: statusArea?.textContent?.trim().substring(0, 100), hasStatusText }; }); if (!result.hasStatusArea && !result.hasStatusText) { return { passed: null, skipped: true, message: 'No status messages area found' }; } return { passed: true, message: result.hasStatusArea ? `Status area found: "${result.statusText?.substring(0, 50)}..."` : 'Status text found in page' }; }, async progressLogPanel(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('[data-research-id], a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : (item?.dataset?.researchId || null); }); if (!researchId) { return { passed: null, skipped: true, message: 'No research to test log panel' }; } await navigateTo(page, `${baseUrl}/progress/${researchId}`); const result = await page.evaluate(() => { const logPanel = document.querySelector( '.log-panel, ' + '.logs, ' + '.research-logs, ' + '[class*="log-container"], ' + '.output-panel, ' + 'pre.logs' ); const logEntries = document.querySelectorAll('.log-entry, .log-line, .log-message'); return { hasLogPanel: !!logPanel, logEntryCount: logEntries.length, sampleLog: logPanel?.textContent?.substring(0, 100) }; }); if (!result.hasLogPanel && result.logEntryCount === 0) { return { passed: null, skipped: true, message: 'No log panel found' }; } return { passed: true, message: result.hasLogPanel ? `Log panel found (${result.logEntryCount} entries)` : `${result.logEntryCount} log entries found` }; }, async progressCancelButton(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); // First check home page for active research await navigateTo(page, `${baseUrl}/`); const homeResult = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button, a.btn')); const cancelBtn = buttons.find(b => { const text = b.textContent?.toLowerCase() || ''; return text.includes('cancel') || text.includes('stop') || text.includes('terminate'); }); return { found: !!cancelBtn, text: cancelBtn?.textContent?.trim() }; }); if (homeResult.found) { return { passed: true, message: `Cancel button found on home: "${homeResult.text}"` }; } // Check progress page const researchId = await page.evaluate(() => { const item = document.querySelector('a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : null; }); if (researchId) { await navigateTo(page, `${baseUrl}/progress/${researchId}`); const progressResult = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button, a.btn')); const cancelBtn = buttons.find(b => { const text = b.textContent?.toLowerCase() || ''; return text.includes('cancel') || text.includes('stop') || text.includes('terminate'); }); return { found: !!cancelBtn, text: cancelBtn?.textContent?.trim() }; }); if (progressResult.found) { return { passed: true, message: `Cancel button found: "${progressResult.text}"` }; } } return { passed: null, skipped: true, message: 'No cancel button found (may only appear during active research)' }; }, async progressPhaseIndicator(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('[data-research-id], a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : null; }); if (!researchId) { return { passed: null, skipped: true, message: 'No research to test phase indicator' }; } await navigateTo(page, `${baseUrl}/progress/${researchId}`); const result = await page.evaluate(() => { const phaseIndicator = document.querySelector( '.phase-indicator, ' + '.current-phase, ' + '.step-indicator, ' + '[class*="phase"], ' + '.research-stage' ); // Look for phase/step text patterns const phasePattern = /(?:phase|step|stage)\s*[:\d]/i; const bodyText = document.body.textContent || ''; const hasPhaseText = phasePattern.test(bodyText); // Look for stepper/timeline component const stepper = document.querySelector('.stepper, .timeline, .steps, .wizard-steps'); return { hasPhaseIndicator: !!phaseIndicator, phaseText: phaseIndicator?.textContent?.trim(), hasPhaseInText: hasPhaseText, hasStepper: !!stepper }; }); if (!result.hasPhaseIndicator && !result.hasPhaseInText && !result.hasStepper) { return { passed: null, skipped: true, message: 'No phase indicator found' }; } return { passed: true, message: result.hasPhaseIndicator ? `Phase indicator: "${result.phaseText}"` : (result.hasStepper ? 'Stepper component found' : 'Phase text found in page') }; }, async progressEstimatedTime(page, baseUrl) { await navigateTo(page, `${baseUrl}/history`); const researchId = await page.evaluate(() => { const item = document.querySelector('[data-research-id], a[href*="/progress/"]'); const match = item?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/); return match ? match[1] : null; }); if (!researchId) { return { passed: null, skipped: true, message: 'No research to test time display' }; } await navigateTo(page, `${baseUrl}/progress/${researchId}`); const result = await page.evaluate(() => { const timeElement = document.querySelector( '.estimated-time, ' + '.time-remaining, ' + '.elapsed-time, ' + '[class*="duration"], ' + '.eta' ); // Look for time patterns const timePattern = /\d+\s*(?:min|sec|hour|[smh])|remaining|elapsed|eta/i; const bodyText = document.body.textContent || ''; const hasTimeText = timePattern.test(bodyText); return { hasTimeElement: !!timeElement, timeText: timeElement?.textContent?.trim(), hasTimeInText: hasTimeText }; }); if (!result.hasTimeElement && !result.hasTimeInText) { return { passed: null, skipped: true, message: 'No time display found' }; } return { passed: true, message: result.hasTimeElement ? `Time display: "${result.timeText}"` : 'Time information found in page' }; } }; // ============================================================================ // Main Test Runner // ============================================================================ async function main() { log.section('Real-time Progress Tests'); const ctx = await setupTest({ authenticate: true }); const results = new TestResults('Real-time Progress Tests'); const { page } = ctx; const { baseUrl } = ctx.config; const subTestTimeout = ctx.config.isCI ? 60000 : 30000; async function run(category, name, testFn) { try { const result = await withTimeout( testFn(page, baseUrl), subTestTimeout, `${category}/${name}` ); if (result.skipped) { results.skip(category, name, result.message); } else { results.add(category, name, result.passed, result.message); } } catch (error) { results.add(category, name, false, `Error: ${error.message}`); } } try { // Progress Page Structure Tests log.section('Progress Page Structure'); // The progress route renders pages/progress.html unconditionally for ANY // research_id (no 404), so the page STRUCTURE is server-side and can be // validated without real research — run it unconditionally so a no-LLM CI // shard actually exercises it (fail, not silently skip). await run('Structure', 'Progress Page Structure', (p, u) => ProgressPageTests.progressPageStructure(p, u)); // The remaining subtests depend on LIVE research data (bar value, status // messages, log entries, percentage, ETA) that only exists during an active // run. On a fresh CI DB with no research, skip those — they aren't dead, they // genuinely require data we can't produce without an LLM. await navigateTo(page, `${baseUrl}/history`); const hasResearch = await page.evaluate(() => { return !!document.querySelector('[data-research-id], [data-id], a[href*="/progress/"], a[href*="/results/"]'); }); if (!hasResearch) { const skipMsg = 'No research history found (fresh CI environment)'; results.skip('Structure', 'Progress Bar Exists', skipMsg); results.skip('Structure', 'Progress Percentage Display', skipMsg); results.skip('Structure', 'Progress Status Messages', skipMsg); results.skip('Structure', 'Progress Log Panel', skipMsg); results.skip('Structure', 'Progress Cancel Button', skipMsg); results.skip('Structure', 'Progress Phase Indicator', skipMsg); results.skip('Structure', 'Progress Estimated Time', skipMsg); } else { await run('Structure', 'Progress Bar Exists', (p, u) => ProgressPageTests.progressBarExists(p, u)); await run('Structure', 'Progress Percentage Display', (p, u) => ProgressPageTests.progressPercentageDisplay(p, u)); await run('Structure', 'Progress Status Messages', (p, u) => ProgressPageTests.progressStatusMessages(p, u)); await run('Structure', 'Progress Log Panel', (p, u) => ProgressPageTests.progressLogPanel(p, u)); await run('Structure', 'Progress Cancel Button', (p, u) => ProgressPageTests.progressCancelButton(p, u)); await run('Structure', 'Progress Phase Indicator', (p, u) => ProgressPageTests.progressPhaseIndicator(p, u)); await run('Structure', 'Progress Estimated Time', (p, u) => ProgressPageTests.progressEstimatedTime(p, u)); } } catch (error) { log.error(`Fatal error: ${error.message}`); console.error(error.stack); } finally { results.print(); results.save(); await teardownTest(ctx); process.exit(results.exitCode()); } } // Run if executed directly if (require.main === module) { main().catch(error => { console.error('Test runner failed:', error); process.exit(1); }); } module.exports = { ProgressPageTests };