Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

446 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});