7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
446 lines
18 KiB
JavaScript
446 lines
18 KiB
JavaScript
const puppeteer = require('puppeteer');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const AuthHelper = require('./auth_helper');
|
||
const { CI_TEST_USER } = require('./auth_helper');
|
||
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
|
||
const { setupDefaultModel } = require('./model_helper');
|
||
|
||
// Test configuration
|
||
const BASE_URL = 'http://127.0.0.1:5000';
|
||
const isCI = !!process.env.CI;
|
||
const TEST_USER = isCI ? CI_TEST_USER.username : `export_test_${Date.now()}`;
|
||
const TEST_PASSWORD = isCI ? CI_TEST_USER.password : 'TestPass123!';
|
||
|
||
// 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 waitForSelectorWithTimeout(page, selector, timeout = 5000) {
|
||
try {
|
||
await page.waitForSelector(selector, { timeout });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
async function configureExportSettings(page) {
|
||
log('\n=== CONFIGURING EXPORT SETTINGS ===', 'section');
|
||
|
||
await page.goto(`${BASE_URL}/settings`);
|
||
await page.waitForSelector('.settings-form, #settings-form');
|
||
|
||
// Enable multiple export formats
|
||
const exportFormats = ['markdown', 'quarto', 'ris', 'latex'];
|
||
|
||
// Look for export format checkboxes or multi-select
|
||
const exportFormatElements = await page.$$('input[name*="export_format"], select[name*="export_format"]');
|
||
|
||
if (exportFormatElements.length > 0) {
|
||
log('📝 Configuring export formats...', 'info');
|
||
|
||
// If it's a multi-select
|
||
const multiSelect = await page.$('select[name*="export_format"][multiple]');
|
||
if (multiSelect) {
|
||
await page.evaluate((formats) => {
|
||
const select = document.querySelector('select[name*="export_format"][multiple]');
|
||
if (select) {
|
||
// Clear existing selections
|
||
Array.from(select.options).forEach(opt => { opt.selected = false; });
|
||
|
||
// Select specified formats
|
||
formats.forEach(format => {
|
||
const option = Array.from(select.options).find(opt =>
|
||
opt.value === format || opt.textContent.toLowerCase().includes(format)
|
||
);
|
||
if (option) option.selected = true;
|
||
});
|
||
|
||
// Trigger change event
|
||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||
}
|
||
}, exportFormats);
|
||
|
||
log('✅ Export formats configured', 'success');
|
||
} else {
|
||
// Handle checkboxes
|
||
for (const format of exportFormats) {
|
||
const checkbox = await page.$(`input[type="checkbox"][name*="${format}"], input[type="checkbox"][value="${format}"]`);
|
||
if (checkbox) {
|
||
const isChecked = await page.evaluate(el => el.checked, checkbox);
|
||
if (!isChecked) {
|
||
await checkbox.click();
|
||
log(` - Enabled ${format} export`, 'info');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Save settings
|
||
const saveButton = await page.$('button[type="submit"], .save-settings-btn');
|
||
if (saveButton) {
|
||
await saveButton.click();
|
||
await delay(2000);
|
||
log('✅ Settings saved', 'success');
|
||
}
|
||
}
|
||
|
||
async function createResearchAndWait(page) {
|
||
log('\n=== CREATING RESEARCH FOR EXPORT TEST ===', 'section');
|
||
|
||
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('#query');
|
||
|
||
// Set up model configuration before submitting
|
||
// NOTE: This is required for CI where no real OLLAMA models exist.
|
||
// Without setupDefaultModel(), research submission fails because:
|
||
// 1. "Filtered models for provider OLLAMA: 0 models"
|
||
// 2. No model is selected in the form
|
||
// 3. The API rejects research with no model
|
||
// See model_helper.js for how it sets #model and #model_hidden inputs.
|
||
log('🔧 Configuring model...', 'info');
|
||
const modelConfigured = await setupDefaultModel(page);
|
||
if (!modelConfigured) {
|
||
log('⚠️ Could not configure model, test may fail', 'warning');
|
||
}
|
||
|
||
// Use a simpler query that completes faster
|
||
const testQuery = 'What is 2+2?';
|
||
await page.type('#query', testQuery);
|
||
|
||
// Submit research and wait for navigation to complete
|
||
// Use Promise.race to handle both navigation and staying on same page
|
||
const submitButton = await page.$('button[type="submit"]');
|
||
|
||
try {
|
||
// Wait for either navigation or a timeout (page stays on home)
|
||
await Promise.race([
|
||
Promise.all([
|
||
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 }),
|
||
submitButton ? submitButton.click() : page.keyboard.press('Enter')
|
||
]),
|
||
// If no navigation happens within 5s, research was queued/async
|
||
delay(5000)
|
||
]);
|
||
} catch {
|
||
// Navigation timeout is fine - page might stay on home page
|
||
log(`ℹ️ No navigation after submit (research may be queued)`, 'info');
|
||
}
|
||
|
||
// Give page time to settle after any navigation
|
||
await delay(1000);
|
||
|
||
// Now check the final URL - page should be stable at this point
|
||
const url = page.url();
|
||
log(`📍 Current URL after submit: ${url}`, 'info');
|
||
|
||
// Check if research was submitted
|
||
// NOTE: The page can navigate to different URLs depending on the app state:
|
||
// - /research/{id} - research in progress
|
||
// - /progress/{id} - progress page (legacy)
|
||
// - /results/{id} - results page
|
||
// - / - home page (if queued or error shown inline)
|
||
const navigatedToResearchPage = url.includes('/research/') ||
|
||
url.includes('/progress/') ||
|
||
url.includes('/results/');
|
||
|
||
// Also check if we stayed on home page but research was accepted
|
||
const urlWithoutParams = url.split('?')[0];
|
||
const stayedOnHomePage = urlWithoutParams === BASE_URL ||
|
||
urlWithoutParams === `${BASE_URL}/`;
|
||
|
||
// If we stayed on home page, check for error messages
|
||
if (stayedOnHomePage) {
|
||
let hasError = null;
|
||
try {
|
||
hasError = await page.evaluate(() => {
|
||
const errorElements = document.querySelectorAll('.alert-danger, .error-message, .toast-error');
|
||
for (const el of errorElements) {
|
||
if (el.textContent.trim()) {
|
||
return el.textContent.trim();
|
||
}
|
||
}
|
||
return null;
|
||
});
|
||
} catch (evalErr) {
|
||
// Context can be destroyed if navigation happens mid-evaluate
|
||
log(`⚠️ Could not check for errors (${evalErr.message.substring(0, 50)}...), continuing...`, 'warning');
|
||
}
|
||
if (hasError) {
|
||
log(`⚠️ Error found on home page: ${hasError}`, 'warning');
|
||
// Don't fail - the export test can still check history page
|
||
}
|
||
}
|
||
|
||
if (navigatedToResearchPage || stayedOnHomePage) {
|
||
log(`✅ Research submitted (URL: ${url})`, 'success');
|
||
|
||
// Wait for research to complete (simplified)
|
||
log('⏳ Waiting for research to start processing...', 'info');
|
||
|
||
// Don't wait for full completion - just verify research started
|
||
// The export functionality should be available even while research is in progress
|
||
await delay(5000);
|
||
|
||
// Check if we have any research content or progress
|
||
let hasContent;
|
||
try {
|
||
hasContent = await page.evaluate(() => {
|
||
// Check for any research-related content
|
||
const indicators = [
|
||
'.progress-info', '.progress-text', '.status',
|
||
'.report-content', '.markdown-content', '.research-results',
|
||
'.research-status', '[class*="progress"]'
|
||
];
|
||
|
||
for (const selector of indicators) {
|
||
const elem = document.querySelector(selector);
|
||
if (elem && elem.textContent.trim()) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
});
|
||
} catch {
|
||
// If there's an error, assume research is processing
|
||
log('⚠️ Could not check research status, continuing...', 'warning');
|
||
hasContent = true;
|
||
}
|
||
|
||
if (hasContent) {
|
||
log('✅ Research is processing', 'success');
|
||
} else {
|
||
log('⚠️ Research status unclear, continuing with test...', 'warning');
|
||
}
|
||
} else {
|
||
throw new Error('Research submission failed');
|
||
}
|
||
|
||
return testQuery;
|
||
}
|
||
|
||
async function testExportFunctionality() {
|
||
const browser = await puppeteer.launch(getPuppeteerLaunchOptions());
|
||
|
||
let page = await browser.newPage();
|
||
|
||
// Set up download handling
|
||
const downloadPath = path.join(__dirname, 'downloads');
|
||
if (!fs.existsSync(downloadPath)) {
|
||
fs.mkdirSync(downloadPath, { recursive: true });
|
||
}
|
||
|
||
// Use the modern CDP API
|
||
const client = await page.createCDPSession();
|
||
await client.send('Page.setDownloadBehavior', {
|
||
behavior: 'allow',
|
||
downloadPath
|
||
});
|
||
|
||
// Set console log handler
|
||
page.on('console', msg => {
|
||
if (msg.type() === 'error' && !msg.text().includes('favicon')) {
|
||
log(`Browser console error: ${msg.text()}`, 'error');
|
||
}
|
||
});
|
||
|
||
try {
|
||
// Setup - authenticate (tries CI user first, falls back to registration)
|
||
const auth = new AuthHelper(page);
|
||
log('🔐 Authenticating...', 'info');
|
||
await auth.ensureAuthenticatedWithTimeout();
|
||
page = auth.page;
|
||
log('✅ Authenticated', 'success');
|
||
await configureExportSettings(page);
|
||
await createResearchAndWait(page);
|
||
|
||
// Navigate to results
|
||
log('\n=== TESTING EXPORT FROM RESULTS PAGE ===', 'section');
|
||
|
||
// Check if we're already on a page with results
|
||
const currentUrl = page.url();
|
||
log(`Current URL: ${currentUrl}`, 'info');
|
||
|
||
// If we're on the progress page and research is complete, results should be visible
|
||
const hasResults = await page.$('.report-content, .markdown-content, .research-results');
|
||
|
||
if (!hasResults) {
|
||
// For testing export functionality, we don't need to wait for results
|
||
// We can test export UI on history page or progress page
|
||
log('Staying on current page to test export UI...', 'info');
|
||
}
|
||
|
||
// Instead of waiting for report content (which may not be ready yet),
|
||
// just verify we're on a page where export functionality should exist
|
||
log('Checking current page for export functionality...', 'info');
|
||
|
||
// Give the page a moment to load any dynamic content
|
||
await delay(2000);
|
||
|
||
// Check if we're on a page that should have export options
|
||
const currentPageUrl = page.url();
|
||
const isOnResultPage = currentPageUrl.includes('/results/') ||
|
||
currentPageUrl.includes('/progress/') ||
|
||
currentPageUrl.includes('/research/') ||
|
||
currentPageUrl.includes('/history');
|
||
|
||
if (!isOnResultPage) {
|
||
log('⚠️ Not on a results page, trying to navigate to history...', 'warning');
|
||
await page.goto(`${BASE_URL}/history`);
|
||
await delay(2000);
|
||
}
|
||
|
||
// Test download links (simplified)
|
||
log('\n=== CHECKING EXPORT FUNCTIONALITY ===', 'section');
|
||
|
||
const exportElements = await page.evaluate(() => {
|
||
const elements = {
|
||
downloadLinks: document.querySelectorAll('a[download], a[href*="download"], .download-link').length,
|
||
exportButtons: document.querySelectorAll('.export-btn, button[data-action="export"], [class*="export"]').length,
|
||
printButtons: document.querySelectorAll('.print-btn, button[data-action="print"]').length +
|
||
Array.from(document.querySelectorAll('button')).filter(b => b.textContent.includes('Print')).length,
|
||
copyButtons: document.querySelectorAll('.copy-btn, button[data-action="copy"]').length +
|
||
Array.from(document.querySelectorAll('button')).filter(b => b.textContent.includes('Copy')).length
|
||
};
|
||
return elements;
|
||
});
|
||
|
||
log('📊 Export elements found:', 'info');
|
||
log(` - Download links: ${exportElements.downloadLinks}`, 'info');
|
||
log(` - Export buttons: ${exportElements.exportButtons}`, 'info');
|
||
log(` - Print buttons: ${exportElements.printButtons}`, 'info');
|
||
log(` - Copy buttons: ${exportElements.copyButtons}`, 'info');
|
||
|
||
const hasExportFunctionality = Object.values(exportElements).some(count => count > 0);
|
||
|
||
if (hasExportFunctionality) {
|
||
log('✅ Export functionality is available', 'success');
|
||
} else {
|
||
log('⚠️ No export functionality found on this page', 'warning');
|
||
}
|
||
|
||
// Quick check for export menu (simplified)
|
||
log('\n=== QUICK EXPORT CHECK ===', 'section');
|
||
|
||
const exportButton = await page.$('.export-btn, button[data-action="export"], [class*="export"]') ||
|
||
await page.evaluate(() => {
|
||
const buttons = Array.from(document.querySelectorAll('.dropdown-toggle, button'));
|
||
return Boolean(buttons.find(b => b.textContent.includes('Export')));
|
||
});
|
||
if (exportButton) {
|
||
log('✅ Export button found', 'success');
|
||
|
||
// Try to click it to verify it works
|
||
try {
|
||
if (typeof exportButton === 'object' && exportButton.click) {
|
||
await exportButton.click();
|
||
await delay(500);
|
||
|
||
// Check if menu opened
|
||
const menuOpened = await page.evaluate(() => {
|
||
return document.querySelectorAll('.export-option, .dropdown-item[data-format], .dropdown-menu.show').length > 0;
|
||
});
|
||
|
||
if (menuOpened) {
|
||
log('✅ Export menu opens successfully', 'success');
|
||
}
|
||
}
|
||
} catch {
|
||
log('⚠️ Could not interact with export button', 'warning');
|
||
}
|
||
} else {
|
||
log('⚠️ No export button found', 'warning');
|
||
}
|
||
|
||
// Quick print check
|
||
const printButton = await page.$('.print-btn, button[data-action="print"]') ||
|
||
await page.evaluate(() => {
|
||
const buttons = Array.from(document.querySelectorAll('button'));
|
||
return Boolean(buttons.find(b => b.textContent.includes('Print')));
|
||
});
|
||
if (printButton) {
|
||
log('✅ Print button found', 'success');
|
||
}
|
||
|
||
// Quick copy check
|
||
const copyButton = await page.$('.copy-btn, button[data-action="copy"]') ||
|
||
await page.evaluate(() => {
|
||
const buttons = Array.from(document.querySelectorAll('button'));
|
||
return Boolean(buttons.find(b => b.textContent.includes('Copy')));
|
||
});
|
||
if (copyButton) {
|
||
log('✅ Copy button found', 'success');
|
||
}
|
||
|
||
// Quick history page check
|
||
log('\n=== QUICK HISTORY CHECK ===', 'section');
|
||
|
||
try {
|
||
await page.goto(`${BASE_URL}/history`, { timeout: 10000 });
|
||
|
||
// Wait for page to load but don't fail if no history items
|
||
const hasHistoryItems = await waitForSelectorWithTimeout(page, '.history-item', 3000);
|
||
|
||
if (hasHistoryItems) {
|
||
const historyExportButtons = await page.$$('.history-item .export-btn, .history-item [data-action="export"]');
|
||
if (historyExportButtons.length > 0) {
|
||
log(`✅ Found ${historyExportButtons.length} export options in history`, 'success');
|
||
} else {
|
||
log('ℹ️ No export buttons found in history items', 'info');
|
||
}
|
||
} else {
|
||
log('ℹ️ No history items found (may be expected for new user)', 'info');
|
||
}
|
||
} catch {
|
||
log('⚠️ Could not check history page exports', 'warning');
|
||
}
|
||
|
||
log('\n✅ Export functionality test completed successfully!', 'success');
|
||
|
||
} catch (error) {
|
||
log(`\n❌ Test failed: ${error.message}`, 'error');
|
||
|
||
throw error;
|
||
} finally {
|
||
// Clean up downloads directory
|
||
if (fs.existsSync(downloadPath)) {
|
||
fs.rmSync(downloadPath, { recursive: true });
|
||
}
|
||
|
||
await browser.close();
|
||
}
|
||
}
|
||
|
||
// Run the test
|
||
testExportFunctionality().catch(error => {
|
||
console.error('Test execution failed:', error);
|
||
process.exit(1);
|
||
});
|