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

265 lines
9.2 KiB
JavaScript

/**
* Test Results Collector
*
* Single source of truth for test result tracking and output.
* Supports JSON, JUnit XML, and console output formats.
*/
const fs = require('fs');
const path = require('path');
/**
* Escape special characters for XML
* @param {string} str - String to escape
* @returns {string}
*/
function escapeXml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
class TestResults {
/**
* Create a new TestResults collector
* @param {string} suiteName - Name of the test suite
*/
constructor(suiteName) {
this.suiteName = suiteName;
this.tests = [];
this.startTime = Date.now();
}
/**
* Add a test result
* @param {string} category - Test category/group
* @param {string} name - Test name
* @param {boolean} passed - Whether the test passed
* @param {string} [message=''] - Result message or error
* @param {number} [duration=0] - Test duration in ms
*/
add(category, name, passed, message = '', duration = 0) {
this.tests.push({
category,
name,
passed,
skipped: false,
message,
duration,
timestamp: new Date().toISOString(),
});
const icon = passed ? '\x1b[32m\u2713\x1b[0m' : '\x1b[31m\u2717\x1b[0m';
console.log(` ${icon} [${category}] ${name}: ${message}`);
}
/**
* Mark a test as skipped
* @param {string} category - Test category/group
* @param {string} name - Test name
* @param {string} [reason=''] - Reason for skipping
*/
skip(category, name, reason = '') {
this.tests.push({
category,
name,
passed: true,
skipped: true,
message: reason,
duration: 0,
timestamp: new Date().toISOString(),
});
console.log(` \x1b[33m\u21B7\x1b[0m [${category}] ${name}: SKIPPED${reason ? ` - ${reason}` : ''}`);
}
/**
* Run a test function and record the result
* @param {string} category - Test category
* @param {string} name - Test name
* @param {Function} testFn - Async test function
* @param {Object} [context] - Optional context (page, etc.) for screenshots on failure
* @returns {Promise<boolean>} Whether the test passed
*/
async run(category, name, testFn, context = {}) {
const startTime = Date.now();
try {
const result = await testFn();
const duration = Date.now() - startTime;
// Handle test functions that return { passed, message }
if (result && typeof result === 'object' && 'passed' in result) {
this.add(category, name, result.passed, result.message || '', duration);
return result.passed;
}
// Test passed if no error was thrown
this.add(category, name, true, 'Passed', duration);
return true;
} catch (error) {
const duration = Date.now() - startTime;
this.add(category, name, false, error.message, duration);
// Take screenshot on failure if page is available
if (context.page && context.screenshotOnFailure !== false) {
try {
const screenshotDir = path.join(__dirname, '..', 'screenshots');
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
const safeName = `${category}_${name}`.replace(/[^a-z0-9]/gi, '_');
const screenshotPath = path.join(screenshotDir, `failure_${safeName}_${Date.now()}.png`);
await context.page.screenshot({ path: screenshotPath, fullPage: true });
console.log(` \x1b[90mScreenshot saved: ${screenshotPath}\x1b[0m`);
} catch (screenshotError) {
console.log(` \x1b[90mFailed to capture screenshot: ${screenshotError.message}\x1b[0m`);
}
}
return false;
}
}
/**
* Get summary statistics
* @returns {Object} Summary with total, passed, failed, skipped, duration
*/
get summary() {
const total = this.tests.length;
const skipped = this.tests.filter(t => t.skipped).length;
const passed = this.tests.filter(t => t.passed && !t.skipped).length;
const failed = total - passed - skipped;
const duration = Date.now() - this.startTime;
return { total, passed, failed, skipped, duration };
}
/**
* Get results as JSON object
* @returns {Object}
*/
toJSON() {
return {
suite: this.suiteName,
summary: this.summary,
tests: this.tests,
};
}
/**
* Get results as JUnit XML
* @returns {string}
*/
toJUnitXML() {
const { summary } = this;
// Group tests by category
const testsByCategory = {};
for (const test of this.tests) {
if (!testsByCategory[test.category]) {
testsByCategory[test.category] = [];
}
testsByCategory[test.category].push(test);
}
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
xml += `<testsuites name="${escapeXml(this.suiteName)}" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${(summary.duration / 1000).toFixed(3)}">\n`;
for (const [category, tests] of Object.entries(testsByCategory)) {
const catPassed = tests.filter(t => t.passed && !t.skipped).length;
const catFailed = tests.filter(t => !t.passed).length;
const catSkipped = tests.filter(t => t.skipped).length;
const catDuration = tests.reduce((sum, t) => sum + (t.duration || 0), 0);
xml += ` <testsuite name="${escapeXml(category)}" tests="${tests.length}" failures="${catFailed}" skipped="${catSkipped}" time="${(catDuration / 1000).toFixed(3)}">\n`;
for (const test of tests) {
xml += ` <testcase classname="${escapeXml(category)}" name="${escapeXml(test.name)}" time="${((test.duration || 0) / 1000).toFixed(3)}"`;
if (test.skipped) {
xml += `>\n <skipped${test.message ? ` message="${escapeXml(test.message)}"` : ''}/>\n </testcase>\n`;
} else if (!test.passed) {
xml += `>\n <failure message="${escapeXml(test.message)}">${escapeXml(test.message)}</failure>\n </testcase>\n`;
} else {
xml += ` />\n`;
}
}
xml += ` </testsuite>\n`;
}
xml += `</testsuites>\n`;
return xml;
}
/**
* Print summary to console
*/
print() {
const { summary } = this;
console.log('\n' + '='.repeat(60));
console.log(`TEST RESULTS: ${this.suiteName}`);
console.log('='.repeat(60));
console.log(`Total: ${summary.total} | Passed: \x1b[32m${summary.passed}\x1b[0m | Failed: \x1b[31m${summary.failed}\x1b[0m | Skipped: \x1b[33m${summary.skipped}\x1b[0m`);
console.log(`Duration: ${(summary.duration / 1000).toFixed(2)}s`);
console.log(`Pass Rate: ${summary.total > 0 ? ((summary.passed / (summary.total - summary.skipped)) * 100).toFixed(1) : 0}%`);
if (summary.failed > 0) {
console.log('\n\x1b[31mFailed tests:\x1b[0m');
this.tests
.filter(t => !t.passed && !t.skipped)
.forEach(t => {
console.log(` \x1b[31m\u2717\x1b[0m [${t.category}] ${t.name}: ${t.message}`);
});
}
console.log('='.repeat(60) + '\n');
}
/**
* Save results to file
* @param {string} [dir] - Output directory
* @param {Object} [options] - Save options
* @param {boolean} [options.json=true] - Save JSON file
* @param {boolean} [options.junit=true] - Save JUnit XML file
*/
save(dir, options = {}) {
const outputDir = dir || path.join(__dirname, '..', 'test-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const safeName = this.suiteName.replace(/[^a-z0-9]/gi, '-').toLowerCase();
if (options.json !== false) {
const jsonPath = path.join(outputDir, `${safeName}.json`);
fs.writeFileSync(jsonPath, JSON.stringify(this.toJSON(), null, 2));
console.log(`JSON results saved: ${jsonPath}`);
}
if (options.junit !== false || process.env.CI) {
const xmlPath = path.join(outputDir, `${safeName}.xml`);
fs.writeFileSync(xmlPath, this.toJUnitXML());
console.log(`JUnit XML saved: ${xmlPath}`);
}
}
/**
* Get appropriate process exit code
* @returns {number} 0 if all tests passed, 1 otherwise
*/
exitCode() {
const { failed, passed, total } = this.summary;
if (failed > 0) return 1;
return 0;
}
}
module.exports = { TestResults, escapeXml };