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

534 lines
21 KiB
JavaScript

#!/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: <div class="ldr-page active" id="research-progress">).
// 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: <div class="ldr-progress-bar" role="progressbar">
// - the percent display: <div id="progress-percentage">
// - the status indicator:<div id="status-text">
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 <h1> 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 };