7a0da7932b
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
371 lines
14 KiB
JavaScript
371 lines
14 KiB
JavaScript
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);
|
||
});
|