chore: import upstream snapshot with attribution
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:55 +08:00
commit 7a0da7932b
2985 changed files with 1049377 additions and 0 deletions
+445
View File
@@ -0,0 +1,445 @@
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);
});