diff --git a/.gitignore b/.gitignore index fc1f70320..4afed9191 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ examples/selenoid-example/output test/data/app/db test/data/sandbox/steps.d.ts test/data/sandbox/configs/custom-reporter-plugin/output/result.json +test/data/sandbox/configs/html-reporter-plugin/output/ +output/ +test/runner/output/ testpullfilecache* .DS_Store package-lock.json diff --git a/README.md b/README.md index 4ca636d91..cbe95200b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ You don't need to worry about asynchronous nature of NodeJS or about various API - Smart locators: use names, labels, matching text, CSS or XPath to locate elements. - 🌐 Interactive debugging shell: pause test at any point and try different commands in a browser. - ⚑ **Parallel testing** with dynamic test pooling for optimal load balancing and performance. +- πŸ“Š **Built-in HTML Reporter** with interactive dashboard, step-by-step execution details, and comprehensive test analytics. - Easily create tests, pageobjects, stepobjects with CLI generators. ## Installation @@ -234,6 +235,49 @@ Scenario('test title', () => { }) ``` +## HTML Reporter + +CodeceptJS includes a powerful built-in HTML Reporter that generates comprehensive, interactive test reports with detailed information about your test runs. The HTML reporter is **enabled by default** for all new projects and provides: + +### Features + +- **Interactive Dashboard**: Visual statistics, pie charts, and expandable test details +- **Step-by-Step Execution**: Shows individual test steps with timing and status indicators +- **BDD/Gherkin Support**: Full support for feature files with proper scenario formatting +- **System Information**: Comprehensive environment details including browser versions +- **Advanced Filtering**: Real-time filtering by status, tags, features, and test types +- **History Tracking**: Multi-run history with trend visualization +- **Error Details**: Clean formatting of error messages and stack traces +- **Artifacts Support**: Display screenshots and other test artifacts + +### Visual Examples + +#### Interactive Test Dashboard + +The main dashboard provides a complete overview with interactive statistics and pie charts: + +![HTML Reporter Dashboard](docs/shared/html-reporter-main-dashboard.png) + +#### Detailed Test Results + +Each test shows comprehensive execution details with expandable step information: + +![HTML Reporter Test Details](docs/shared/html-reporter-test-details.png) + +#### Advanced Filtering Capabilities + +Real-time filtering allows quick navigation through test results: + +![HTML Reporter Filtering](docs/shared/html-reporter-filtering.png) + +#### BDD/Gherkin Support + +Full support for Gherkin scenarios with proper feature formatting: + +![HTML Reporter BDD Details](docs/shared/html-reporter-bdd-details.png) + +The HTML reporter generates self-contained reports that can be easily shared with your team. Learn more about configuration and features in the [HTML Reporter documentation](https://codecept.io/plugins/#htmlreporter). + ## PageObjects CodeceptJS provides the most simple way to create and use page objects in your test. diff --git a/docs/plugins.md b/docs/plugins.md index d726e636a..641c8f39d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -714,6 +714,44 @@ More config options are available: - `config` (optional, default `{}`) +## htmlReporter + +HTML Reporter Plugin for CodeceptJS + +Generates comprehensive HTML reports showing: + +- Test statistics +- Feature/Scenario details +- Individual step results +- Test artifacts (screenshots, etc.) + +## Configuration + +```js +"plugins": { + "htmlReporter": { + "enabled": true, + "output": "./output", + "reportFileName": "report.html", + "includeArtifacts": true, + "showSteps": true, + "showSkipped": true, + "showMetadata": true, + "showTags": true, + "showRetries": true, + "exportStats": false, + "exportStatsPath": "./stats.json", + "keepHistory": false, + "historyPath": "./test-history.json", + "maxHistoryEntries": 50 + } +} +``` + +### Parameters + +- `config` + ## pageInfo Collects information from web page after each failed test and adds it to the test as an artifact. diff --git a/docs/reports.md b/docs/reports.md index bf444dfb3..07bf89bad 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -228,6 +228,66 @@ Result will be located at `output/result.xml` file. ## Html +### Built-in HTML Reporter + +CodeceptJS includes a built-in HTML reporter plugin that generates comprehensive HTML reports with detailed test information. + +#### Features + +- **Interactive Test Results**: Click on tests to expand and view detailed information +- **Step-by-Step Details**: Shows individual test steps with status indicators and timing +- **Test Statistics**: Visual cards showing totals, passed, failed, and pending test counts +- **Error Information**: Detailed error messages for failed tests with clean formatting +- **Artifacts Support**: Display screenshots and other test artifacts with modal viewing +- **Responsive Design**: Mobile-friendly layout that works on all screen sizes +- **Professional Styling**: Modern, clean interface with color-coded status indicators + +#### Configuration + +Add the `htmlReporter` plugin to your `codecept.conf.js`: + +```js +exports.config = { + // ... your other configuration + plugins: { + htmlReporter: { + enabled: true, + output: './output', // Directory for the report + reportFileName: 'report.html', // Name of the HTML file + includeArtifacts: true, // Include screenshots/artifacts + showSteps: true, // Show individual test steps + showSkipped: true // Show skipped tests + } + } +} +``` + +#### Configuration Options + +- `output` (optional, default: `./output`) - Directory where the HTML report will be saved +- `reportFileName` (optional, default: `'report.html'`) - Name of the generated HTML file +- `includeArtifacts` (optional, default: `true`) - Whether to include screenshots and other artifacts +- `showSteps` (optional, default: `true`) - Whether to display individual test steps +- `showSkipped` (optional, default: `true`) - Whether to include skipped tests in the report + +#### Usage + +Run your tests normally and the HTML report will be automatically generated: + +```sh +npx codeceptjs run +``` + +The report will be saved to `output/report.html` (or your configured location) and includes: + +- Overview statistics with visual cards +- Expandable test details showing steps and timing +- Error messages for failed tests +- Screenshots and artifacts (if available) +- Interactive failures section + +### Mochawesome + Best HTML reports could be produced with [mochawesome](https://www.npmjs.com/package/mochawesome) reporter. ![mochawesome](/img/mochawesome.png) diff --git a/docs/shared/html-reporter-bdd-details.png b/docs/shared/html-reporter-bdd-details.png new file mode 100644 index 000000000..56db49b86 Binary files /dev/null and b/docs/shared/html-reporter-bdd-details.png differ diff --git a/docs/shared/html-reporter-filtering.png b/docs/shared/html-reporter-filtering.png new file mode 100644 index 000000000..519608324 Binary files /dev/null and b/docs/shared/html-reporter-filtering.png differ diff --git a/docs/shared/html-reporter-main-dashboard.png b/docs/shared/html-reporter-main-dashboard.png new file mode 100644 index 000000000..79bc68506 Binary files /dev/null and b/docs/shared/html-reporter-main-dashboard.png differ diff --git a/docs/shared/html-reporter-test-details.png b/docs/shared/html-reporter-test-details.png new file mode 100644 index 000000000..6227a158b Binary files /dev/null and b/docs/shared/html-reporter-test-details.png differ diff --git a/lib/command/init.js b/lib/command/init.js index 735e16c23..ee5ba1294 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -18,6 +18,11 @@ const defaultConfig = { output: '', helpers: {}, include: {}, + plugins: { + htmlReporter: { + enabled: true, + }, + }, } const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe'] diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js new file mode 100644 index 000000000..ec033050e --- /dev/null +++ b/lib/plugin/htmlReporter.js @@ -0,0 +1,1947 @@ +const fs = require('fs') +const path = require('path') +const mkdirp = require('mkdirp') +const crypto = require('crypto') +const { template } = require('../utils') +const { getMachineInfo } = require('../command/info') + +const event = require('../event') +const output = require('../output') +const Codecept = require('../codecept') + +const defaultConfig = { + output: global.output_dir || './output', + reportFileName: 'report.html', + includeArtifacts: true, + showSteps: true, + showSkipped: true, + showMetadata: true, + showTags: true, + showRetries: true, + exportStats: false, + exportStatsPath: './stats.json', + keepHistory: false, + historyPath: './test-history.json', + maxHistoryEntries: 50, +} + +/** + * HTML Reporter Plugin for CodeceptJS + * + * Generates comprehensive HTML reports showing: + * - Test statistics + * - Feature/Scenario details + * - Individual step results + * - Test artifacts (screenshots, etc.) + * + * ## Configuration + * + * ```js + * "plugins": { + * "htmlReporter": { + * "enabled": true, + * "output": "./output", + * "reportFileName": "report.html", + * "includeArtifacts": true, + * "showSteps": true, + * "showSkipped": true, + * "showMetadata": true, + * "showTags": true, + * "showRetries": true, + * "exportStats": false, + * "exportStatsPath": "./stats.json", + * "keepHistory": false, + * "historyPath": "./test-history.json", + * "maxHistoryEntries": 50 + * } + * } + * ``` + */ +module.exports = function (config) { + const options = { ...defaultConfig, ...config } + let reportData = { + stats: {}, + tests: [], + failures: [], + hooks: [], + startTime: null, + endTime: null, + retries: [], + config: options, + } + let currentTestSteps = [] + let currentTestHooks = [] + let currentBddSteps = [] // Track BDD/Gherkin steps + let testRetryAttempts = new Map() // Track retry attempts per test + let currentSuite = null // Track current suite for BDD detection + + // Initialize report directory + const reportDir = options.output ? path.resolve(global.codecept_dir, options.output) : path.resolve(global.output_dir || './output') + mkdirp.sync(reportDir) + + // Track overall test execution + event.dispatcher.on(event.all.before, () => { + reportData.startTime = new Date() + output.plugin('htmlReporter', 'Starting HTML report generation...') + }) + + // Track test start to initialize steps and hooks collection + event.dispatcher.on(event.test.before, test => { + currentTestSteps = [] + currentTestHooks = [] + currentBddSteps = [] + + // Track current suite for BDD detection + currentSuite = test.parent + + // Track retry attempts + if (test.retriedTest && test.retriedTest()) { + const originalTest = test.retriedTest() + const testId = generateTestId(originalTest) + if (!testRetryAttempts.has(testId)) { + testRetryAttempts.set(testId, 0) + } + testRetryAttempts.set(testId, testRetryAttempts.get(testId) + 1) + } + }) + + // Collect step information + event.dispatcher.on(event.step.started, step => { + step.htmlReporterStartTime = Date.now() + }) + + event.dispatcher.on(event.step.finished, step => { + if (step.htmlReporterStartTime) { + step.duration = Date.now() - step.htmlReporterStartTime + } + currentTestSteps.push({ + name: step.name, + actor: step.actor, + args: step.args || [], + status: step.failed ? 'failed' : 'success', + duration: step.duration || 0, + }) + }) + + // Collect hook information + event.dispatcher.on(event.hook.started, hook => { + hook.htmlReporterStartTime = Date.now() + }) + + event.dispatcher.on(event.hook.finished, hook => { + if (hook.htmlReporterStartTime) { + hook.duration = Date.now() - hook.htmlReporterStartTime + } + const hookInfo = { + title: hook.title, + type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite + status: hook.err ? 'failed' : 'passed', + duration: hook.duration || 0, + error: hook.err ? hook.err.message : null, + } + currentTestHooks.push(hookInfo) + reportData.hooks.push(hookInfo) + }) + + // Collect BDD/Gherkin step information + event.dispatcher.on(event.bddStep.started, step => { + step.htmlReporterStartTime = Date.now() + }) + + event.dispatcher.on(event.bddStep.finished, step => { + if (step.htmlReporterStartTime) { + step.duration = Date.now() - step.htmlReporterStartTime + } + currentBddSteps.push({ + keyword: step.actor || 'Given', + text: step.name, + status: step.failed ? 'failed' : 'success', + duration: step.duration || 0, + comment: step.comment, + }) + }) + + // Collect test results + event.dispatcher.on(event.test.finished, test => { + const testId = generateTestId(test) + const retryAttempts = testRetryAttempts.get(testId) || 0 + + // Detect if this is a BDD/Gherkin test + const isBddTest = isBddGherkinTest(test, currentSuite) + const steps = isBddTest ? currentBddSteps : currentTestSteps + const featureInfo = isBddTest ? getBddFeatureInfo(test, currentSuite) : null + + reportData.tests.push({ + ...test, + id: testId, + duration: test.duration || 0, + steps: [...steps], // Copy the steps (BDD or regular) + hooks: [...currentTestHooks], // Copy the hooks + artifacts: test.artifacts || [], + tags: test.tags || [], + meta: test.meta || {}, + opts: test.opts || {}, + notes: test.notes || [], + retryAttempts: retryAttempts, + uid: test.uid, + isBdd: isBddTest, + feature: featureInfo, + }) + + // If this was a retry, track the retry information + if (retryAttempts> 0) { + reportData.retries.push({ + testId: testId, + testTitle: test.title, + attempts: retryAttempts, + finalState: test.state, + duration: test.duration || 0, + }) + } + }) + + // Generate final report + event.dispatcher.on(event.all.result, result => { + reportData.endTime = new Date() + reportData.stats = result.stats + reportData.failures = result.failures || [] + reportData.duration = reportData.endTime - reportData.startTime + + generateHtmlReport(reportData, options) + + // Export stats if configured + if (options.exportStats) { + exportTestStats(reportData, options) + } + + // Save history if configured + if (options.keepHistory) { + saveTestHistory(reportData, options) + } + }) + + function generateTestId(test) { + return crypto + .createHash('sha256') + .update(`${test.parent?.title || 'unknown'}_${test.title}`) + .digest('hex') + .substring(0, 8) + } + + function isBddGherkinTest(test, suite) { + // Check if the suite has BDD/Gherkin properties + return !!(suite && (suite.feature || suite.file?.endsWith('.feature'))) + } + + function getBddFeatureInfo(test, suite) { + if (!suite) return null + + return { + name: suite.feature?.name || suite.title, + description: suite.feature?.description || suite.comment || '', + language: suite.feature?.language || 'en', + tags: suite.tags || [], + file: suite.file || '', + } + } + + function exportTestStats(data, config) { + const statsPath = path.resolve(reportDir, config.exportStatsPath) + + const exportData = { + timestamp: data.endTime.toISOString(), + duration: data.duration, + stats: data.stats, + retries: data.retries, + testCount: data.tests.length, + passedTests: data.tests.filter(t => t.state === 'passed').length, + failedTests: data.tests.filter(t => t.state === 'failed').length, + pendingTests: data.tests.filter(t => t.state === 'pending').length, + tests: data.tests.map(test => ({ + id: test.id, + title: test.title, + feature: test.parent?.title || 'Unknown', + state: test.state, + duration: test.duration, + tags: test.tags, + meta: test.meta, + retryAttempts: test.retryAttempts, + uid: test.uid, + })), + } + + try { + fs.writeFileSync(statsPath, JSON.stringify(exportData, null, 2)) + output.print(`Test stats exported to: ${statsPath}`) + } catch (error) { + output.print(`Failed to export test stats: ${error.message}`) + } + } + + function saveTestHistory(data, config) { + const historyPath = path.resolve(reportDir, config.historyPath) + let history = [] + + // Load existing history + try { + if (fs.existsSync(historyPath)) { + history = JSON.parse(fs.readFileSync(historyPath, 'utf8')) + } + } catch (error) { + output.print(`Failed to load existing history: ${error.message}`) + } + + // Add current run to history + history.unshift({ + timestamp: data.endTime.toISOString(), + duration: data.duration, + stats: data.stats, + retries: data.retries.length, + testCount: data.tests.length, + }) + + // Limit history entries + if (history.length> config.maxHistoryEntries) { + history = history.slice(0, config.maxHistoryEntries) + } + + try { + fs.writeFileSync(historyPath, JSON.stringify(history, null, 2)) + output.print(`Test history saved to: ${historyPath}`) + } catch (error) { + output.print(`Failed to save test history: ${error.message}`) + } + } + + async function generateHtmlReport(data, config) { + const reportPath = path.join(reportDir, config.reportFileName) + + // Load history if available + let history = [] + if (config.keepHistory) { + const historyPath = path.resolve(reportDir, config.historyPath) + try { + if (fs.existsSync(historyPath)) { + history = JSON.parse(fs.readFileSync(historyPath, 'utf8')).slice(0, 10) // Last 10 runs for chart + } + } catch (error) { + output.print(`Failed to load history for report: ${error.message}`) + } + } + + // Get system information + const systemInfo = await getMachineInfo() + + const html = template(getHtmlTemplate(), { + title: `CodeceptJS Test Report v${Codecept.version()}`, + timestamp: data.endTime.toISOString(), + duration: formatDuration(data.duration), + stats: JSON.stringify(data.stats), + history: JSON.stringify(history), + statsHtml: generateStatsHtml(data.stats), + testsHtml: generateTestsHtml(data.tests, config), + failuresHtml: generateFailuresHtml(data.failures), + retriesHtml: config.showRetries ? generateRetriesHtml(data.retries) : '', + cssStyles: getCssStyles(), + jsScripts: getJsScripts(), + showRetries: config.showRetries ? 'block' : 'none', + showHistory: config.keepHistory && history.length> 0 ? 'block' : 'none', + failuresDisplay: data.failures && data.failures.length> 0 ? 'block' : 'none', + codeceptVersion: Codecept.version(), + systemInfoHtml: generateSystemInfoHtml(systemInfo), + }) + + fs.writeFileSync(reportPath, html) + output.print(`HTML Report saved to: ${reportPath}`) + } + + function generateStatsHtml(stats) { + const passed = stats.passes || 0 + const failed = stats.failures || 0 + const pending = stats.pending || 0 + const total = stats.tests || 0 + + return ` +
+
+

Total

+ ${total} +
+
+

Passed

+ ${passed} +
+
+

Failed

+ ${failed} +
+
+

Pending

+ ${pending} +
+
+
+ + +
+ ` + } + + function generateTestsHtml(tests, config) { + if (!tests || tests.length === 0) { + return '

No tests found.

' + } + + return tests + .map(test => { + const statusClass = test.state || 'unknown' + const feature = test.isBdd && test.feature ? test.feature.name : test.parent?.title || 'Unknown Feature' + const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : '' + const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : '' + const hooks = test.hooks && test.hooks.length> 0 ? generateHooksHtml(test.hooks) : '' + const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts) : '' + const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : '' + const tags = config.showTags && test.tags && test.tags.length> 0 ? generateTagsHtml(test.tags) : '' + const retries = config.showRetries && test.retryAttempts> 0 ? generateTestRetryHtml(test.retryAttempts) : '' + const notes = test.notes && test.notes.length> 0 ? generateNotesHtml(test.notes) : '' + + return ` +
+
+ ●くろまる +
+

${test.isBdd ? `Scenario: ${test.title}` : test.title}

+
+ ${test.isBdd ? 'Feature: ' : ''}${feature} + ${test.uid ? `${test.uid}` : ''} + ${formatDuration(test.duration)} + ${test.retryAttempts> 0 ? `${test.retryAttempts} retries` : ''} + ${test.isBdd ? 'Gherkin' : ''} +
+
+
+
+ ${test.err ? `
${escapeHtml(test.err.message || '').replace(/\x1b\[[0-9;]*m/g, '')}
` : ''} + ${featureDetails} + ${tags} + ${metadata} + ${retries} + ${notes} + ${hooks} + ${steps} + ${artifacts} +
+
+ ` + }) + .join('') + } + + function generateStepsHtml(steps) { + if (!steps || steps.length === 0) return '' + + const stepsHtml = steps + .map(step => { + const statusClass = step.status || 'unknown' + const args = step.args ? step.args.map(arg => JSON.stringify(arg)).join(', ') : '' + const stepName = step.name || 'unknown step' + const actor = step.actor || 'I' + + return ` +
+ ●くろまる + ${actor}.${stepName}(${args}) + ${formatDuration(step.duration)} +
+ ` + }) + .join('') + + return ` +
+

Steps:

+
${stepsHtml}
+
+ ` + } + + function generateBddStepsHtml(steps) { + if (!steps || steps.length === 0) return '' + + const stepsHtml = steps + .map(step => { + const statusClass = step.status || 'unknown' + const keyword = step.keyword || 'Given' + const text = step.text || '' + const comment = step.comment ? `
${escapeHtml(step.comment)}
` : '' + + return ` +
+ ●くろまる + ${keyword} + ${escapeHtml(text)} + ${formatDuration(step.duration)} + ${comment} +
+ ` + }) + .join('') + + return ` +
+

Scenario Steps:

+
${stepsHtml}
+
+ ` + } + + function generateBddFeatureHtml(feature) { + if (!feature) return '' + + const description = feature.description ? `
${escapeHtml(feature.description)}
` : '' + const featureTags = feature.tags && feature.tags.length> 0 ? `
${feature.tags.map(tag => `${escapeHtml(tag)}`).join('')}
` : '' + + return ` +
+

Feature Information:

+
+
Feature: ${escapeHtml(feature.name)}
+ ${description} + ${featureTags} + ${feature.file ? `
File: ${escapeHtml(feature.file)}
` : ''} +
+
+ ` + } + + function generateHooksHtml(hooks) { + if (!hooks || hooks.length === 0) return '' + + const hooksHtml = hooks + .map(hook => { + const statusClass = hook.status || 'unknown' + const hookType = hook.type || 'hook' + const hookTitle = hook.title || `${hookType} hook` + + return ` +
+ ●くろまる + ${hookType}: ${hookTitle} + ${formatDuration(hook.duration)} + ${hook.error ? `
${escapeHtml(hook.error)}
` : ''} +
+ ` + }) + .join('') + + return ` +
+

Hooks:

+
${hooksHtml}
+
+ ` + } + + function generateMetadataHtml(meta, opts) { + const allMeta = { ...(opts || {}), ...(meta || {}) } + if (!allMeta || Object.keys(allMeta).length === 0) return '' + + const metaHtml = Object.entries(allMeta) + .filter(([key, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + const displayValue = typeof value === 'object' ? JSON.stringify(value) : value.toString() + return `
${escapeHtml(key)}: ${escapeHtml(displayValue)}
` + }) + .join('') + + return ` +
+

Metadata:

+
${metaHtml}
+
+ ` + } + + function generateTagsHtml(tags) { + if (!tags || tags.length === 0) return '' + + const tagsHtml = tags.map(tag => `${escapeHtml(tag)}`).join('') + + return ` +
+

Tags:

+
${tagsHtml}
+
+ ` + } + + function generateNotesHtml(notes) { + if (!notes || notes.length === 0) return '' + + const notesHtml = notes.map(note => `
${note.type || 'info'}: ${escapeHtml(note.text)}
`).join('') + + return ` +
+

Notes:

+
${notesHtml}
+
+ ` + } + + function generateTestRetryHtml(retryAttempts) { + return ` +
+

Retry Information:

+
+ Total retry attempts: ${retryAttempts} +
+
+ ` + } + + function generateArtifactsHtml(artifacts) { + if (!artifacts || artifacts.length === 0) return '' + + const artifactsHtml = artifacts + .map(artifact => { + if (typeof artifact === 'string' && artifact.match(/\.(png|jpg|jpeg|gif)$/i)) { + const relativePath = path.relative(reportDir, artifact) + return `Screenshot` + } + return `
${escapeHtml(artifact.toString())}
` + }) + .join('') + + return ` +
+

Artifacts:

+
${artifactsHtml}
+
+ ` + } + + function generateFailuresHtml(failures) { + if (!failures || failures.length === 0) { + return '

No failures.

' + } + + return failures + .map((failure, index) => { + const failureText = failure.toString().replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI escape codes + return ` +
+

Failure ${index + 1}

+
${escapeHtml(failureText)}
+
+ ` + }) + .join('') + } + + function generateRetriesHtml(retries) { + if (!retries || retries.length === 0) { + return '

No retried tests.

' + } + + return retries + .map( + retry => ` +
+

${retry.testTitle}

+
+ Attempts: ${retry.attempts} + Final State: ${retry.finalState} + Duration: ${formatDuration(retry.duration)} +
+
+ `, + ) + .join('') + } + + function formatDuration(duration) { + if (!duration) return '0ms' + if (duration < 1000) return `${duration}ms` + return `${(duration / 1000).toFixed(2)}s` + } + + function escapeHtml(unsafe) { + return unsafe.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') + } + + function generateSystemInfoHtml(systemInfo) { + if (!systemInfo) return '' + + const formatInfo = (key, value) => { + if (Array.isArray(value) && value.length> 1) { + return `
${key}: ${escapeHtml(value[1])}
` + } else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') { + return `
${key}: ${escapeHtml(value)}
` + } + return '' + } + + const infoItems = [ + formatInfo('Node.js', systemInfo.nodeInfo), + formatInfo('OS', systemInfo.osInfo), + formatInfo('CPU', systemInfo.cpuInfo), + formatInfo('Chrome', systemInfo.chromeInfo), + formatInfo('Edge', systemInfo.edgeInfo), + formatInfo('Firefox', systemInfo.firefoxInfo), + formatInfo('Safari', systemInfo.safariInfo), + formatInfo('Playwright Browsers', systemInfo.playwrightBrowsers), + ] + .filter(item => item) + .join('') + + if (!infoItems) return '' + + return ` +
+
+

Environment Information

+ β–Ό +
+
+
+ ${infoItems} +
+
+
+ ` + } + + function getHtmlTemplate() { + return ` + + + + + + {{title}} + + +
+
+

{{title}}

+
+ Generated: {{timestamp}} + Duration: {{duration}} +
+
+ +
+ {{systemInfoHtml}} + +
+

Test Statistics

+ {{statsHtml}} +
+ +
+

Test History

+
+ +
+
+ +
+

Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Test Results

+
+ {{testsHtml}} +
+
+ +
+

Test Retries

+
+ {{retriesHtml}} +
+
+ +
+

Failures

+
+ {{failuresHtml}} +
+
+
+ + +
+ +
+ + + +

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /

+ + ` + } + + function getCssStyles() { + return ` +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background-color: #f5f5f5; +} + +.report-header { + background: #2c3e50; + color: white; + padding: 2rem 1rem; + text-align: center; +} + +.report-header h1 { + margin-bottom: 0.5rem; + font-size: 2.5rem; +} + +.report-meta { + font-size: 0.9rem; + opacity: 0.8; +} + +.report-meta span { + margin: 0 1rem; +} + +.report-content { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +.stats-section, .tests-section, .failures-section, .retries-section, .filters-section, .history-section, .system-info-section { + background: white; + margin-bottom: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + overflow: hidden; +} + +.stats-section h2, .tests-section h2, .failures-section h2, .retries-section h2, .filters-section h2, .history-section h2 { + background: #34495e; + color: white; + padding: 1rem; + margin: 0; +} + +.stats-cards { + display: flex; + flex-wrap: wrap; + gap: 1rem; + padding: 1rem; +} + +.stat-card { + flex: 1; + min-width: 150px; + padding: 1rem; + text-align: center; + border-radius: 4px; + color: white; +} + +.stat-card.total { background: #3498db; } +.stat-card.passed { background: #27ae60; } +.stat-card.failed { background: #e74c3c; } +.stat-card.pending { background: #f39c12; } + +.stat-card h3 { + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.stat-number { + font-size: 2rem; + font-weight: bold; +} + +.pie-chart-container { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem 1rem; + background: white; + margin: 1rem 0; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +#statsChart { + max-width: 100%; + height: auto; +} + +.test-item { + border-bottom: 1px solid #eee; + margin: 0; +} + +.test-item:last-child { + border-bottom: none; +} + +.test-header { + display: flex; + align-items: center; + padding: 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.test-header:hover { + background-color: #f8f9fa; +} + +.test-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.test-meta-line { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; +} + +.test-status { + font-size: 1.2rem; + margin-right: 0.5rem; +} + +.test-status.passed { color: #27ae60; } +.test-status.failed { color: #e74c3c; } +.test-status.pending { color: #f39c12; } +.test-status.skipped { color: #95a5a6; } + +.test-title { + font-size: 1.1rem; + font-weight: 500; + margin: 0; +} + +.test-feature { + background: #ecf0f1; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + color: #34495e; +} + +.test-uid { + background: #e8f4fd; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + color: #2980b9; + font-family: monospace; +} + +.retry-badge { + background: #f39c12; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: bold; +} + +.test-duration { + font-size: 0.8rem; + color: #7f8c8d; +} + +.test-details { + display: none; + padding: 1rem; + background: #f8f9fa; + border-top: 1px solid #e9ecef; +} + +.error-message { + background: #fee; + border: 1px solid #fcc; + border-radius: 4px; + padding: 1rem; + margin-bottom: 1rem; +} + +.error-message pre { + color: #c0392b; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + white-space: pre-wrap; + word-wrap: break-word; +} + +.steps-section, .artifacts-section, .hooks-section { + margin-top: 1rem; +} + +.steps-section h4, .artifacts-section h4, .hooks-section h4 { + color: #34495e; + margin-bottom: 0.5rem; + font-size: 1rem; +} + +.hook-item { + display: flex; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #ecf0f1; +} + +.hook-item:last-child { + border-bottom: none; +} + +.hook-status { + margin-right: 0.5rem; +} + +.hook-status.passed { color: #27ae60; } +.hook-status.failed { color: #e74c3c; } + +.hook-title { + flex: 1; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + font-weight: bold; +} + +.hook-duration { + font-size: 0.8rem; + color: #7f8c8d; +} + +.hook-error { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem; + background: #fee; + border: 1px solid #fcc; + border-radius: 4px; + color: #c0392b; + font-size: 0.8rem; +} + +.step-item { + display: flex; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #ecf0f1; +} + +.step-item:last-child { + border-bottom: none; +} + +.step-status { + margin-right: 0.5rem; +} + +.step-status.success { color: #27ae60; } +.step-status.failed { color: #e74c3c; } + +.step-title { + flex: 1; + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +.step-duration { + font-size: 0.8rem; + color: #7f8c8d; +} + +.artifacts-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.artifact-image { + max-width: 200px; + max-height: 150px; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + transition: transform 0.2s; +} + +.artifact-image:hover { + transform: scale(1.05); +} + +.artifact-item { + background: #ecf0f1; + padding: 0.5rem; + border-radius: 4px; + font-size: 0.9rem; +} + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + cursor: pointer; +} + +.modal img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 90%; + max-height: 90%; + border-radius: 4px; +} + +.failure-item { + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid #fcc; + border-radius: 4px; + background: #fee; +} + +.failure-item h4 { + color: #c0392b; + margin-bottom: 0.5rem; +} + +.failure-details { + color: #333; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Filter Controls */ +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 1rem; + padding: 1rem; + background: #f8f9fa; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.filter-group label { + font-size: 0.9rem; + font-weight: 500; + color: #34495e; +} + +.filter-group input, +.filter-group select { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + min-width: 150px; +} + +.filter-group select[multiple] { + height: auto; + min-height: 80px; +} + +.filter-controls button { + padding: 0.5rem 1rem; + background: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + align-self: flex-end; +} + +.filter-controls button:hover { + background: #2980b9; +} + +/* Test Tags */ +.tags-section, .metadata-section, .notes-section, .retry-section { + margin-top: 1rem; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.test-tag { + background: #3498db; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.8rem; +} + +/* Metadata */ +.metadata-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.meta-item { + padding: 0.5rem; + background: #f8f9fa; + border-radius: 4px; + border-left: 3px solid #3498db; +} + +.meta-key { + font-weight: bold; + color: #2c3e50; +} + +.meta-value { + color: #34495e; + font-family: monospace; +} + +/* Notes */ +.notes-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.note-item { + padding: 0.5rem; + border-radius: 4px; + border-left: 3px solid #95a5a6; +} + +.note-item.note-info { + background: #e8f4fd; + border-left-color: #3498db; +} + +.note-item.note-warning { + background: #fef9e7; + border-left-color: #f39c12; +} + +.note-item.note-error { + background: #fee; + border-left-color: #e74c3c; +} + +.note-item.note-retry { + background: #f0f8e8; + border-left-color: #27ae60; +} + +.note-type { + font-weight: bold; + text-transform: uppercase; + font-size: 0.8rem; +} + +/* Retry Information */ +.retry-info { + padding: 0.5rem; + background: #fef9e7; + border-radius: 4px; + border-left: 3px solid #f39c12; +} + +.retry-count { + color: #d68910; + font-weight: 500; +} + +/* Retries Section */ +.retry-item { + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid #f39c12; + border-radius: 4px; + background: #fef9e7; +} + +.retry-item h4 { + color: #d68910; + margin-bottom: 0.5rem; +} + +.retry-details { + display: flex; + gap: 1rem; + align-items: center; + font-size: 0.9rem; +} + +.status-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: bold; + text-transform: uppercase; +} + +.status-badge.passed { + background: #27ae60; + color: white; +} + +.status-badge.failed { + background: #e74c3c; + color: white; +} + +.status-badge.pending { + background: #f39c12; + color: white; +} + +/* History Chart */ +.history-chart-container { + padding: 2rem 1rem; + display: flex; + justify-content: center; +} + +#historyChart { + max-width: 100%; + height: auto; +} + +/* Hidden items for filtering */ +.test-item.filtered-out { + display: none !important; +} + +/* System Info Section */ +.system-info-section { + background: white; + margin-bottom: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + overflow: hidden; +} + +.system-info-header { + background: #2c3e50; + color: white; + padding: 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: background-color 0.2s; +} + +.system-info-header:hover { + background: #34495e; +} + +.system-info-header h3 { + margin: 0; + font-size: 1.2rem; +} + +.toggle-icon { + font-size: 1rem; + transition: transform 0.3s ease; +} + +.toggle-icon.rotated { + transform: rotate(-180deg); +} + +.system-info-content { + display: none; + padding: 1.5rem; + background: #f8f9fa; + border-top: 1px solid #e9ecef; +} + +.system-info-content.visible { + display: block; +} + +.system-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; +} + +.info-item { + padding: 0.75rem; + background: white; + border-radius: 6px; + border-left: 4px solid #3498db; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.info-key { + font-weight: bold; + color: #2c3e50; + display: inline-block; + min-width: 100px; +} + +.info-value { + color: #34495e; + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +/* BDD/Gherkin specific styles */ +.bdd-test { + border-left: 4px solid #8e44ad; +} + +.bdd-badge { + background: #8e44ad; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: bold; +} + +.bdd-feature-section { + margin-top: 1rem; + padding: 1rem; + background: #f8f9fa; + border-left: 4px solid #8e44ad; + border-radius: 4px; +} + +.feature-name { + font-weight: bold; + font-size: 1.1rem; + color: #8e44ad; + margin-bottom: 0.5rem; +} + +.feature-description { + color: #34495e; + font-style: italic; + margin: 0.5rem 0; + padding: 0.5rem; + background: white; + border-radius: 4px; +} + +.feature-file { + font-size: 0.8rem; + color: #7f8c8d; + margin-top: 0.5rem; +} + +.feature-tags { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin: 0.5rem 0; +} + +.feature-tag { + background: #8e44ad; + color: white; + padding: 0.2rem 0.4rem; + border-radius: 8px; + font-size: 0.7rem; +} + +.bdd-steps-section { + margin-top: 1rem; +} + +.bdd-steps-section h4 { + color: #8e44ad; + margin-bottom: 0.5rem; + font-size: 1rem; +} + +.bdd-step-item { + display: flex; + align-items: flex-start; + padding: 0.5rem 0; + border-bottom: 1px solid #ecf0f1; + font-family: 'Segoe UI', sans-serif; +} + +.bdd-step-item:last-child { + border-bottom: none; +} + +.bdd-keyword { + font-weight: bold; + color: #8e44ad; + margin-right: 0.5rem; + min-width: 60px; + text-align: left; +} + +.bdd-step-text { + flex: 1; + color: #2c3e50; + margin-right: 0.5rem; +} + +.step-comment { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + color: #6c757d; + font-family: 'Courier New', monospace; + font-size: 0.8rem; + white-space: pre-wrap; +} + +@media (max-width: 768px) { + .stats-cards { + flex-direction: column; + } + + .test-header { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + + .test-feature, .test-duration { + align-self: flex-start; + } +} + ` + } + + function getJsScripts() { + return ` +function toggleTestDetails(testId) { + const details = document.getElementById('details-' + testId); + if (details.style.display === 'none' || details.style.display === '') { + details.style.display = 'block'; + } else { + details.style.display = 'none'; + } +} + +function openImageModal(src) { + const modal = document.getElementById('imageModal'); + const modalImg = document.getElementById('modalImage'); + modalImg.src = src; + modal.style.display = 'block'; +} + +function closeImageModal() { + const modal = document.getElementById('imageModal'); + modal.style.display = 'none'; +} + +function toggleSystemInfo() { + const content = document.getElementById('systemInfoContent'); + const icon = document.querySelector('.toggle-icon'); + + if (content.classList.contains('visible')) { + content.classList.remove('visible'); + icon.classList.remove('rotated'); + } else { + content.classList.add('visible'); + icon.classList.add('rotated'); + } +} + +// Filter functionality +function applyFilters() { + const statusFilter = Array.from(document.getElementById('statusFilter').selectedOptions).map(opt => opt.value); + const featureFilter = document.getElementById('featureFilter').value.toLowerCase(); + const tagFilter = document.getElementById('tagFilter').value.toLowerCase(); + const retryFilter = document.getElementById('retryFilter').value; + const typeFilter = document.getElementById('typeFilter').value; + + const testItems = document.querySelectorAll('.test-item'); + + testItems.forEach(item => { + let shouldShow = true; + + // Status filter + if (statusFilter.length> 0) { + const testStatus = item.dataset.status; + if (!statusFilter.includes(testStatus)) { + shouldShow = false; + } + } + + // Feature filter + if (featureFilter && shouldShow) { + const feature = (item.dataset.feature || '').toLowerCase(); + if (!feature.includes(featureFilter)) { + shouldShow = false; + } + } + + // Tag filter + if (tagFilter && shouldShow) { + const tags = (item.dataset.tags || '').toLowerCase(); + if (!tags.includes(tagFilter)) { + shouldShow = false; + } + } + + // Retry filter + if (retryFilter !== 'all' && shouldShow) { + const retries = parseInt(item.dataset.retries || '0'); + if (retryFilter === 'retried' && retries === 0) { + shouldShow = false; + } else if (retryFilter === 'no-retries' && retries> 0) { + shouldShow = false; + } + } + + // Test type filter (BDD/Gherkin vs Regular) + if (typeFilter !== 'all' && shouldShow) { + const testType = item.dataset.type || 'regular'; + if (typeFilter !== testType) { + shouldShow = false; + } + } + + if (shouldShow) { + item.classList.remove('filtered-out'); + } else { + item.classList.add('filtered-out'); + } + }); + + updateFilteredStats(); +} + +function resetFilters() { + document.getElementById('statusFilter').selectedIndex = -1; + document.getElementById('featureFilter').value = ''; + document.getElementById('tagFilter').value = ''; + document.getElementById('retryFilter').value = 'all'; + document.getElementById('typeFilter').value = 'all'; + + document.querySelectorAll('.test-item').forEach(item => { + item.classList.remove('filtered-out'); + }); + + updateFilteredStats(); +} + +function updateFilteredStats() { + const visibleTests = document.querySelectorAll('.test-item:not(.filtered-out)'); + const totalVisible = visibleTests.length; + + // Update the title to show filtered count + const testsSection = document.querySelector('.tests-section h2'); + const totalTests = document.querySelectorAll('.test-item').length; + + if (totalVisible !== totalTests) { + testsSection.textContent = 'Test Results (' + totalVisible + ' of ' + totalTests + ' shown)'; + } else { + testsSection.textContent = 'Test Results'; + } +} + +// Draw pie chart using canvas +function drawPieChart() { + const canvas = document.getElementById('statsChart'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const data = window.chartData; + + if (!data) return; + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const radius = Math.min(centerX, centerY) - 20; + + const total = data.passed + data.failed + data.pending; + if (total === 0) { + // Draw empty circle for no tests + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); + ctx.stroke(); + ctx.fillStyle = '#888'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('No Tests', centerX, centerY); + return; + } + + let currentAngle = -Math.PI / 2; // Start from top + + // Draw passed segment + if (data.passed> 0) { + const angle = (data.passed / total) * 2 * Math.PI; + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle); + ctx.closePath(); + ctx.fillStyle = '#27ae60'; + ctx.fill(); + currentAngle += angle; + } + + // Draw failed segment + if (data.failed> 0) { + const angle = (data.failed / total) * 2 * Math.PI; + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle); + ctx.closePath(); + ctx.fillStyle = '#e74c3c'; + ctx.fill(); + currentAngle += angle; + } + + // Draw pending segment + if (data.pending> 0) { + const angle = (data.pending / total) * 2 * Math.PI; + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle); + ctx.closePath(); + ctx.fillStyle = '#f39c12'; + ctx.fill(); + } + + // Add legend + const legendY = centerY + radius + 40; + ctx.font = '14px Arial'; + ctx.textAlign = 'left'; + + let legendX = centerX - 120; + + // Passed legend + ctx.fillStyle = '#27ae60'; + ctx.fillRect(legendX, legendY, 15, 15); + ctx.fillStyle = '#333'; + ctx.fillText('Passed (' + data.passed + ')', legendX + 20, legendY + 12); + + // Failed legend + legendX += 100; + ctx.fillStyle = '#e74c3c'; + ctx.fillRect(legendX, legendY, 15, 15); + ctx.fillStyle = '#333'; + ctx.fillText('Failed (' + data.failed + ')', legendX + 20, legendY + 12); + + // Pending legend + if (data.pending> 0) { + legendX += 90; + ctx.fillStyle = '#f39c12'; + ctx.fillRect(legendX, legendY, 15, 15); + ctx.fillStyle = '#333'; + ctx.fillText('Pending (' + data.pending + ')', legendX + 20, legendY + 12); + } +} + +// Draw history chart +function drawHistoryChart() { + const canvas = document.getElementById('historyChart'); + if (!canvas || !window.testData.history || window.testData.history.length === 0) return; + + const ctx = canvas.getContext('2d'); + const history = window.testData.history.slice().reverse(); // Most recent last + + const padding = 50; + const chartWidth = canvas.width - 2 * padding; + const chartHeight = canvas.height - 2 * padding; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Find max values + const maxTests = Math.max(...history.map(h => h.stats.tests || 0)); + const maxDuration = Math.max(...history.map(h => h.duration || 0)); + + if (maxTests === 0) return; + + // Draw axes + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, canvas.height - padding); + ctx.lineTo(canvas.width - padding, canvas.height - padding); + ctx.stroke(); + + // Draw grid lines + ctx.strokeStyle = '#eee'; + ctx.lineWidth = 1; + for (let i = 1; i <= 5; i++) { + const y = padding + (chartHeight * i / 5); + ctx.beginPath(); + ctx.moveTo(padding, y); + ctx.lineTo(canvas.width - padding, y); + ctx.stroke(); + } + + // Draw pass/fail rates + const stepX = chartWidth / (history.length - 1); + + // Draw passed tests line + ctx.strokeStyle = '#27ae60'; + ctx.lineWidth = 3; + ctx.beginPath(); + history.forEach((run, index) => { + const x = padding + (index * stepX); + const y = canvas.height - padding - ((run.stats.passes || 0) / maxTests) * chartHeight; + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + + // Draw failed tests line + ctx.strokeStyle = '#e74c3c'; + ctx.lineWidth = 3; + ctx.beginPath(); + history.forEach((run, index) => { + const x = padding + (index * stepX); + const y = canvas.height - padding - ((run.stats.failures || 0) / maxTests) * chartHeight; + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + + // Add labels + ctx.fillStyle = '#333'; + ctx.font = '12px Arial'; + ctx.textAlign = 'center'; + + // Y-axis labels + ctx.textAlign = 'right'; + for (let i = 0; i <= 5; i++) { + const value = Math.round((maxTests * i) / 5); + const y = canvas.height - padding - (chartHeight * i / 5); + ctx.fillText(value.toString(), padding - 10, y + 4); + } + + // Legend + ctx.textAlign = 'left'; + ctx.fillStyle = '#27ae60'; + ctx.fillRect(padding, 20, 15, 15); + ctx.fillStyle = '#333'; + ctx.fillText('Passed Tests', padding + 20, 32); + + ctx.fillStyle = '#e74c3c'; + ctx.fillRect(padding + 120, 20, 15, 15); + ctx.fillStyle = '#333'; + ctx.fillText('Failed Tests', padding + 140, 32); +} + +// Initialize - hide failures section if no failures and draw charts +document.addEventListener('DOMContentLoaded', function() { + const failuresSection = document.querySelector('.failures-section'); + const failureItems = document.querySelectorAll('.failure-item'); + if (failureItems.length === 0) { + failuresSection.style.display = 'none'; + } + + // Draw charts + drawPieChart(); + drawHistoryChart(); + + // Set up filter event listeners + document.getElementById('statusFilter').addEventListener('change', applyFilters); + document.getElementById('featureFilter').addEventListener('input', applyFilters); + document.getElementById('tagFilter').addEventListener('input', applyFilters); + document.getElementById('retryFilter').addEventListener('change', applyFilters); + document.getElementById('typeFilter').addEventListener('change', applyFilters); +}); + ` + } +} diff --git a/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js b/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js new file mode 100644 index 000000000..ea647cbd8 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js @@ -0,0 +1,19 @@ +Feature('HTML Reporter with Artifacts Test') + +Scenario('test with artifacts', async ({ I }) => { + I.amInPath('.') + I.seeFile('codecept.conf.js') + + // Simulate adding test artifacts + const container = require('../../../../../lib/container') + try { + const currentTest = container.mocha().currentTest + if (currentTest) { + currentTest.artifacts = currentTest.artifacts || [] + currentTest.artifacts.push('fake-screenshot-1.png') + currentTest.artifacts.push('fake-screenshot-2.png') + } + } catch (e) { + // Ignore if container not available + } +}) diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.js new file mode 100644 index 000000000..faf46f210 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.js @@ -0,0 +1,31 @@ +exports.config = { + tests: './*_test.js', + timeout: 10000, + output: './output', + helpers: { + FileSystem: {}, + }, + gherkin: { + features: './features/*.feature', + steps: './step_definitions/steps.js', + }, + include: {}, + bootstrap: false, + mocha: {}, + name: 'sandbox-bdd', + plugins: { + htmlReporter: { + enabled: true, + output: './output', + reportFileName: 'bdd-report.html', + includeArtifacts: true, + showSteps: true, + showSkipped: true, + showMetadata: true, + showTags: true, + showRetries: true, + exportStats: false, + keepHistory: false, + }, + }, +} \ No newline at end of file diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.js new file mode 100644 index 000000000..8949ea5a3 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.js @@ -0,0 +1,27 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + plugins: { + htmlReporter: { + enabled: true, + output: './output', + reportFileName: 'report.html', + includeArtifacts: true, + showSteps: true, + showSkipped: true, + showMetadata: true, + showTags: true, + showRetries: true, + keepHistory: true, + historyPath: './test-history.json', + maxHistoryEntries: 10, + }, + }, + mocha: {}, + name: 'html-reporter-plugin tests with history', +} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.js new file mode 100644 index 000000000..a64c5c2d3 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.js @@ -0,0 +1,26 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + plugins: { + htmlReporter: { + enabled: true, + output: './output', + reportFileName: 'report.html', + includeArtifacts: true, + showSteps: true, + showSkipped: true, + showMetadata: true, + showTags: true, + showRetries: true, + exportStats: true, + exportStatsPath: './test-stats.json', + }, + }, + mocha: {}, + name: 'html-reporter-plugin tests with stats', +} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.js new file mode 100644 index 000000000..61e085e6c --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.js @@ -0,0 +1,21 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + plugins: { + htmlReporter: { + enabled: true, + output: './output', + reportFileName: 'report.html', + includeArtifacts: true, + showSteps: true, + showSkipped: true, + }, + }, + mocha: {}, + name: 'html-reporter-plugin tests', +} \ No newline at end of file diff --git a/test/data/sandbox/configs/html-reporter-plugin/features/html-reporter.feature b/test/data/sandbox/configs/html-reporter-plugin/features/html-reporter.feature new file mode 100644 index 000000000..b275314b3 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/features/html-reporter.feature @@ -0,0 +1,29 @@ +@html-reporter @smoke +Feature: HTML Reporter BDD Test + In order to verify BDD support in HTML reporter + As a developer + I want to see properly formatted Gherkin scenarios + + Background: + Given I setup the test environment + + @important + Scenario: Basic BDD test scenario + Given I have a basic setup + When I perform an action + Then I should see the expected result + And everything should work correctly + + @regression @critical + Scenario: Test with data table + Given I have the following items: + | name | price | + | Item 1 | 10 | + | Item 2 | 20 | + When I process the items + Then the total should be 30 + + Scenario: Test that will fail + Given I have a setup that will fail + When I perform a failing action + Then this step will not be reached \ No newline at end of file diff --git a/test/data/sandbox/configs/html-reporter-plugin/html-reporter_test.js b/test/data/sandbox/configs/html-reporter-plugin/html-reporter_test.js new file mode 100644 index 000000000..1ec50a97d --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/html-reporter_test.js @@ -0,0 +1,16 @@ +Feature('HTML Reporter Test') + +Scenario('test with multiple steps', ({ I }) => { + I.amInPath('.') + I.seeFile('package.json') +}) + +Scenario('test that will fail', ({ I }) => { + I.amInPath('.') + I.seeFile('this-file-should-not-exist.txt') +}) + +Scenario('test that will pass', ({ I }) => { + I.amInPath('.') + I.seeFile('codecept.conf.js') +}) \ No newline at end of file diff --git a/test/data/sandbox/configs/html-reporter-plugin/package.json b/test/data/sandbox/configs/html-reporter-plugin/package.json new file mode 100644 index 000000000..d82476379 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/package.json @@ -0,0 +1,11 @@ +{ + "name": "html-reporter-plugin-test", + "version": "1.0.0", + "description": "Test package for HTML reporter plugin tests", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/test/data/sandbox/configs/html-reporter-plugin/step_definitions/steps.js b/test/data/sandbox/configs/html-reporter-plugin/step_definitions/steps.js new file mode 100644 index 000000000..3696fcbf7 --- /dev/null +++ b/test/data/sandbox/configs/html-reporter-plugin/step_definitions/steps.js @@ -0,0 +1,46 @@ +const { I } = inject() + +Given('I setup the test environment', () => { + console.log('Setting up test environment') +}) + +Given('I have a basic setup', () => { + console.log('Basic setup completed') +}) + +When('I perform an action', () => { + console.log('Performing action') +}) + +Then('I should see the expected result', () => { + console.log('Expected result verified') +}) + +Then('everything should work correctly', () => { + console.log('Everything working correctly') +}) + +Given('I have the following items:', (table) => { + const data = table.parse() + console.log('Items:', data) +}) + +When('I process the items', () => { + console.log('Processing items') +}) + +Then('the total should be {int}', (total) => { + console.log('Total verified:', total) +}) + +Given('I have a setup that will fail', () => { + console.log('Setup that will fail') +}) + +When('I perform a failing action', () => { + throw new Error('This is an intentional failure for testing') +}) + +Then('this step will not be reached', () => { + console.log('This should not be reached') +}) \ No newline at end of file diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js new file mode 100644 index 000000000..75c6345b4 --- /dev/null +++ b/test/runner/html-reporter-plugin_test.js @@ -0,0 +1,169 @@ +const { expect } = require('expect') +const exec = require('child_process').exec +const { codecept_dir, codecept_run } = require('./consts') +const debug = require('debug')('codeceptjs:tests') +const fs = require('fs') +const path = require('path') + +const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose ? '--verbose' : ''} --config ${codecept_dir}/configs/html-reporter-plugin/${config} ${grep ? `--grep "${grep}"` : ''}` + +describe('CodeceptJS html-reporter-plugin', function () { + this.timeout(10000) + + it('should generate HTML report', done => { + exec(config_run_config('codecept.conf.js'), (err, stdout) => { + debug(stdout) + + // Check if HTML report file exists + const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html') + expect(fs.existsSync(reportFile)).toBe(true) + + // Read and validate HTML report content + const reportContent = fs.readFileSync(reportFile, 'utf8') + expect(reportContent).toContain('CodeceptJS Test Report') + expect(reportContent).toContain('Test Statistics') + expect(reportContent).toContain('Test Results') + + // Check for specific test features + expect(reportContent).toContain('HTML Reporter Test') // Feature name + expect(reportContent).toContain('test with multiple steps') // Scenario name + expect(reportContent).toContain('test that will fail') // Another scenario + expect(reportContent).toContain('test that will pass') // Another scenario + + // Validate that stats are included + expect(reportContent).toMatch(/Total.*Passed.*Failed/s) + + // Check for pie chart functionality + expect(reportContent).toContain('pie-chart-container') + expect(reportContent).toContain('statsChart') + expect(reportContent).toContain('drawPieChart') + expect(reportContent).toMatch(/window\.chartData\s*=/) + + // Check for enhanced features + expect(reportContent).toContain('filter-controls') + expect(reportContent).toContain('statusFilter') + expect(reportContent).toContain('featureFilter') + expect(reportContent).toContain('tagFilter') + expect(reportContent).toContain('retryFilter') + expect(reportContent).toContain('applyFilters') + expect(reportContent).toContain('resetFilters') + + // Check for metadata and tags support + expect(reportContent).toContain('metadata-section') + expect(reportContent).toContain('tags-section') + expect(reportContent).toContain('notes-section') + expect(reportContent).toContain('retry-section') + + // Check for hooks styles (even if not used in this test) + expect(reportContent).toContain('hooks-section') + expect(reportContent).toContain('hook-item') + + // Check basic HTML structure + expect(reportContent).toContain('') + expect(reportContent).toContain('') + expect(reportContent).toContain('') + expect(reportContent).toContain('') + + // Should contain CSS and JS + expect(reportContent).toContain('