diff --git a/README.md b/README.md index 8a80fac30..eb4bd9deb 100644 --- a/README.md +++ b/README.md @@ -233,49 +233,62 @@ Scenario('test title', () => { }) ``` -## HTML Reporter +## HTML Reports -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: +Generate beautiful HTML reports using **[@testomatio/reporter](https://github.com/testomatio/reporter)** package. -### 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 +### Installation -### Visual Examples +```sh +npm install @testomatio/reporter --save-dev +``` -#### Interactive Test Dashboard +### Configuration -The main dashboard provides a complete overview with interactive statistics and pie charts: +Add the `testomatio` plugin to your `codecept.conf.js`: -![HTML Reporter Dashboard](docs/shared/html-reporter-main-dashboard.png) +```js +plugins: { + testomatio: { + enabled: true, + require: '@testomatio/reporter/lib/adapter/codecept', + }, +} +``` -#### Detailed Test Results +### Usage -Each test shows comprehensive execution details with expandable step information: +Generate HTML reports by setting the `TESTOMATIO_HTML_REPORT_SAVE` environment variable: -![HTML Reporter Test Details](docs/shared/html-reporter-test-details.png) +```sh +TESTOMATIO_HTML_REPORT_SAVE=1 npx codeceptjs run +``` -#### Advanced Filtering Capabilities +The report will be saved to `html-report/testomatio-report.html`. -Real-time filtering allows quick navigation through test results: +### Features -![HTML Reporter Filtering](docs/shared/html-reporter-filtering.png) +- Modern, responsive interface with real-time statistics +- Detailed test information with step-by-step breakdown +- Screenshots, videos, and artifacts display +- BDD/Gherkin support with proper scenario formatting +- Customizable output location and filename +- Optional integration with [Testomat.io](https://testomat.io) cloud -#### BDD/Gherkin Support +### Customization -Full support for Gherkin scenarios with proper feature formatting: +```sh +# Custom output folder +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO_HTML_REPORT_FOLDER=./reports npx codeceptjs run -![HTML Reporter BDD Details](docs/shared/html-reporter-bdd-details.png) +# Custom filename +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO_HTML_FILENAME=my-report.html npx codeceptjs run -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). +# Integrate with Testomat.io cloud +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO=your_api_key npx codeceptjs run +``` +Learn more in the [Testomat.io Reporter documentation](https://docs.testomat.io/test-reporting/pipes/html/). ## 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 1a279cd37..d79ba0dfa 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -523,44 +523,6 @@ 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 0f138fc89..54f55a17a 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -236,64 +236,57 @@ codeceptjs run ## Html -### Built-in HTML Reporter +### Testomat.io HTML Reporter -CodeceptJS includes a built-in HTML reporter plugin that generates comprehensive HTML reports with detailed test information. +For modern HTML reports, use **[@testomatio/reporter](https://github.com/testomatio/reporter)** package. -#### Features +#### Installation -- **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 +```sh +npm install @testomatio/reporter --save-dev +``` #### Configuration -Add the `htmlReporter` plugin to your `codecept.conf.js`: +Add the `testomatio` plugin to your `codecept.conf.js`: ```js exports.config = { - // ... your other configuration plugins: { - htmlReporter: { + testomatio: { 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 + require: '@testomatio/reporter/lib/adapter/codecept', }, }, } ``` -#### 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: +Generate HTML reports with environment variable: ```sh -npx codeceptjs run +TESTOMATIO_HTML_REPORT_SAVE=1 npx codeceptjs run ``` -The report will be saved to `output/report.html` (or your configured location) and includes: +Report is saved to `html-report/testomatio-report.html`. + +#### Configuration Options + +Customize report location: + +```sh +# Custom folder +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO_HTML_REPORT_FOLDER=./reports npx codeceptjs run + +# Custom filename +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO_HTML_FILENAME=my-report.html npx codeceptjs run -- 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 +# With Testomat.io cloud +TESTOMATIO_HTML_REPORT_SAVE=1 TESTOMATIO=your_api_key npx codeceptjs run +``` +For more details, see [documentation](https://docs.testomat.io/test-reporting/pipes/html/). ### Mochawesome Best HTML reports could be produced with [mochawesome](https://www.npmjs.com/package/mochawesome) reporter. diff --git a/docs/shared/html-reporter-bdd-details.png b/docs/shared/html-reporter-bdd-details.png deleted file mode 100644 index 56db49b86..000000000 Binary files a/docs/shared/html-reporter-bdd-details.png and /dev/null differ diff --git a/docs/shared/html-reporter-filtering.png b/docs/shared/html-reporter-filtering.png deleted file mode 100644 index 519608324..000000000 Binary files a/docs/shared/html-reporter-filtering.png and /dev/null differ diff --git a/docs/shared/html-reporter-main-dashboard.png b/docs/shared/html-reporter-main-dashboard.png deleted file mode 100644 index 79bc68506..000000000 Binary files a/docs/shared/html-reporter-main-dashboard.png and /dev/null differ diff --git a/docs/shared/html-reporter-test-details.png b/docs/shared/html-reporter-test-details.png deleted file mode 100644 index 6227a158b..000000000 Binary files a/docs/shared/html-reporter-test-details.png and /dev/null differ diff --git a/lib/command/init.js b/lib/command/init.js index 882a792ad..589ec51be 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -20,9 +20,6 @@ const defaultConfig = { helpers: {}, include: {}, plugins: { - htmlReporter: { - enabled: true, - }, }, } diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js deleted file mode 100644 index c762d8fae..000000000 --- a/lib/plugin/htmlReporter.js +++ /dev/null @@ -1,3648 +0,0 @@ -// @ts-nocheck -// TypeScript: Import Node.js types for process, fs, path, etc. -/// - -import fs from 'fs' -import path from 'path' -import { mkdirp } from 'mkdirp' -import crypto from 'crypto' -import { threadId } from 'worker_threads' -import { template } from '../utils.js' -import { getMachineInfo } from '../command/info.js' - -import event from '../event.js' -import output from '../output.js' -import Codecept from '../codecept.js' - -const defaultConfig = { - output: typeof global !== 'undefined' && global.output_dir ? 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 - * } - * } - * ``` - */ -export default function (config) { - const options = { ...defaultConfig, ...config } - /** - * TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors - */ - 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().toISOString() - output.print('HTML Reporter: 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 - - // Enhanced retry detection with priority-based approach - const testId = generateTestId(test) - - // Only set retry count if not already set, using priority order - if (!testRetryAttempts.has(testId)) { - // Method 1: Check retryNum property (most reliable) - if (test.retryNum && test.retryNum > 0) { - testRetryAttempts.set(testId, test.retryNum) - output.debug(`HTML Reporter: Retry count detected (retryNum) for ${test.title}, attempts: ${test.retryNum}`) - } - // Method 2: Check currentRetry property - else if (test.currentRetry && test.currentRetry > 0) { - testRetryAttempts.set(testId, test.currentRetry) - output.debug(`HTML Reporter: Retry count detected (currentRetry) for ${test.title}, attempts: ${test.currentRetry}`) - } - // Method 3: Check if this is a retried test - else if (test.retriedTest && test.retriedTest()) { - const originalTest = test.retriedTest() - const originalTestId = generateTestId(originalTest) - if (!testRetryAttempts.has(originalTestId)) { - testRetryAttempts.set(originalTestId, 1) // Start with 1 retry - } else { - testRetryAttempts.set(originalTestId, testRetryAttempts.get(originalTestId) + 1) - } - output.debug(`HTML Reporter: Retry detected (retriedTest) for ${originalTest.title}, attempts: ${testRetryAttempts.get(originalTestId)}`) - } - // Method 4: Check if test has been seen before (indicating a retry) - else if (reportData.tests.some(t => t.id === testId)) { - testRetryAttempts.set(testId, 1) // First retry detected - output.debug(`HTML Reporter: Retry detected (duplicate test) for ${test.title}, attempts: 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.htmlReporterDuration = Date.now() - step.htmlReporterStartTime - } - - // Serialize args immediately to preserve them through worker serialization - let serializedArgs = [] - if (step.args && Array.isArray(step.args)) { - serializedArgs = step.args.map(arg => { - try { - // Try to convert to JSON-friendly format - if (typeof arg === 'string') return arg - if (typeof arg === 'number') return arg - if (typeof arg === 'boolean') return arg - if (arg === null || arg === undefined) return arg - // For objects, try to serialize them - return JSON.parse(JSON.stringify(arg)) - } catch (e) { - // If serialization fails, convert to string - return String(arg) - } - }) - } - - currentTestSteps.push({ - name: step.name, - actor: step.actor, - args: serializedArgs, - status: step.failed ? 'failed' : 'success', - duration: step.htmlReporterDuration || 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 - } - // Enhanced hook info: include type, name, location, error, and context - 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 || hook.err.toString() : null, - location: hook.file || hook.location || (hook.ctx && hook.ctx.test && hook.ctx.test.file) || null, - context: hook.ctx - ? { - testTitle: hook.ctx.test?.title, - suiteTitle: hook.ctx.test?.parent?.title, - feature: hook.ctx.test?.parent?.feature?.name, - } - : 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.htmlReporterDuration = Date.now() - step.htmlReporterStartTime - } - currentBddSteps.push({ - keyword: step.actor || 'Given', - text: step.name, - status: step.failed ? 'failed' : 'success', - duration: step.htmlReporterDuration || step.duration || 0, - comment: step.comment, - }) - }) - - // Collect skipped tests - event.dispatcher.on(event.test.skipped, test => { - const testId = generateTestId(test) - - // Detect if this is a BDD/Gherkin test - const suite = test.parent || test.suite || currentSuite - const isBddTest = isBddGherkinTest(test, suite) - const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null - - // Extract parent/suite title - const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null - const suiteTitle = test.suite?.title || (suite && suite.title) || null - - const testData = { - ...test, - id: testId, - state: 'pending', // Use 'pending' as the state for skipped tests - duration: 0, - steps: [], - hooks: [], - artifacts: [], - tags: test.tags || [], - meta: test.meta || {}, - opts: test.opts || {}, - notes: test.notes || [], - retryAttempts: 0, - uid: test.uid, - isBdd: isBddTest, - feature: featureInfo, - parentTitle: parentTitle, - suiteTitle: suiteTitle, - } - - reportData.tests.push(testData) - output.debug(`HTML Reporter: Added skipped test - ${test.title}`) - }) - - // Collect test results - event.dispatcher.on(event.test.finished, test => { - const testId = generateTestId(test) - let retryAttempts = testRetryAttempts.get(testId) || 0 - - // Additional retry detection in test.finished event - // Check if this test has retry indicators we might have missed - if (retryAttempts === 0) { - if (test.retryNum && test.retryNum > 0) { - retryAttempts = test.retryNum - testRetryAttempts.set(testId, retryAttempts) - output.debug(`HTML Reporter: Late retry detection (retryNum) for ${test.title}, attempts: ${retryAttempts}`) - } else if (test.currentRetry && test.currentRetry > 0) { - retryAttempts = test.currentRetry - testRetryAttempts.set(testId, retryAttempts) - output.debug(`HTML Reporter: Late retry detection (currentRetry) for ${test.title}, attempts: ${retryAttempts}`) - } else if (test._retries && test._retries > 0) { - retryAttempts = test._retries - testRetryAttempts.set(testId, retryAttempts) - output.debug(`HTML Reporter: Late retry detection (_retries) for ${test.title}, attempts: ${retryAttempts}`) - } - } - - // Debug logging - output.debug(`HTML Reporter: Test finished - ${test.title}, State: ${test.state}, Retries: ${retryAttempts}`) - - // Detect if this is a BDD/Gherkin test - use test.parent directly instead of currentSuite - const suite = test.parent || test.suite || currentSuite - const isBddTest = isBddGherkinTest(test, suite) - const steps = isBddTest ? currentBddSteps : currentTestSteps - const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null - - // Check if this test already exists in reportData.tests (from a previous retry) - const existingTestIndex = reportData.tests.findIndex(t => t.id === testId) - const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex] && reportData.tests[existingTestIndex].state === 'failed' - const currentlyFailed = test.state === 'failed' - - // Debug artifacts collection (but don't process them yet - screenshots may not be ready) - output.debug(`HTML Reporter: Test ${test.title} artifacts at test.finished: ${JSON.stringify(test.artifacts)}`) - - // Extract parent/suite title before serialization (for worker mode) - // This ensures the feature name is preserved when test data is JSON stringified - const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null - const suiteTitle = test.suite?.title || (suite && suite.title) || null - - const testData = { - ...test, - id: testId, - duration: test.duration || 0, - steps: [...steps], // Copy the steps (BDD or regular) - hooks: [...currentTestHooks], // Copy the hooks - artifacts: test.artifacts || [], // Keep original artifacts for now - tags: test.tags || [], - meta: test.meta || {}, - opts: test.opts || {}, - notes: test.notes || [], - retryAttempts: currentlyFailed || hasFailedBefore ? retryAttempts : 0, // Only show retries for failed tests - uid: test.uid, - isBdd: isBddTest, - feature: featureInfo, - // Store parent/suite titles as simple strings for worker mode serialization - parentTitle: parentTitle, - suiteTitle: suiteTitle, - } - - if (existingTestIndex >= 0) { - // Update existing test with final result (including failed state) - if (existingTestIndex >= 0) reportData.tests[existingTestIndex] = testData - output.debug(`HTML Reporter: Updated existing test - ${test.title}, Final state: ${test.state}`) - } else { - // Add new test - reportData.tests.push(testData) - output.debug(`HTML Reporter: Added new test - ${test.title}, State: ${test.state}`) - } - - // Track retry information - only add if there were actual retries AND the test failed at some point - const existingRetryIndex = reportData.retries.findIndex(r => r.testId === testId) - - // Only track retries if: - // 1. There are retry attempts detected AND (test failed now OR failed before) - // 2. OR there's an existing retry record (meaning it failed before) - if ((retryAttempts > 0 && (currentlyFailed || hasFailedBefore)) || existingRetryIndex >= 0) { - // If no retry attempts detected but we have an existing retry record, increment it - if (retryAttempts === 0 && existingRetryIndex >= 0) { - retryAttempts = reportData.retries[existingRetryIndex].attempts + 1 - testRetryAttempts.set(testId, retryAttempts) - output.debug(`HTML Reporter: Incremented retry count for duplicate test ${test.title}, attempts: ${retryAttempts}`) - } - - // Remove existing retry info for this test and add updated one - reportData.retries = reportData.retries.filter(r => r.testId !== testId) - reportData.retries.push({ - testId: testId, - testTitle: test.title, - attempts: retryAttempts, - finalState: test.state, - duration: test.duration || 0, - }) - output.debug(`HTML Reporter: Added retry info for ${test.title}, attempts: ${retryAttempts}, state: ${test.state}`) - } - - // Fallback: If this test already exists and either failed before or is failing now, it's a retry - else if (existingTestIndex >= 0 && (hasFailedBefore || currentlyFailed)) { - const fallbackAttempts = 1 - testRetryAttempts.set(testId, fallbackAttempts) - reportData.retries.push({ - testId: testId, - testTitle: test.title, - attempts: fallbackAttempts, - finalState: test.state, - duration: test.duration || 0, - }) - output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`) - } - }) - - // Generate final report - event.dispatcher.on(event.all.result, async result => { - reportData.endTime = new Date().toISOString() - reportData.duration = new Date(reportData.endTime).getTime() - new Date(reportData.startTime).getTime() - - // Process artifacts now that all async tasks (including screenshots) are complete - output.debug(`HTML Reporter: Processing artifacts for ${reportData.tests.length} tests after all async tasks complete`) - - reportData.tests.forEach(test => { - const originalArtifacts = test.artifacts - let collectedArtifacts = [] - - output.debug(`HTML Reporter: Processing test "${test.title}" (ID: ${test.id})`) - output.debug(`HTML Reporter: Test ${test.title} final artifacts: ${JSON.stringify(originalArtifacts)}`) - - if (originalArtifacts) { - if (Array.isArray(originalArtifacts)) { - collectedArtifacts = originalArtifacts - output.debug(`HTML Reporter: Using array artifacts: ${collectedArtifacts.length} items`) - } else if (typeof originalArtifacts === 'object') { - // Convert object properties to array (screenshotOnFail plugin format) - collectedArtifacts = Object.values(originalArtifacts).filter(artifact => artifact) - output.debug(`HTML Reporter: Converted artifacts object to array: ${collectedArtifacts.length} items`) - output.debug(`HTML Reporter: Converted artifacts: ${JSON.stringify(collectedArtifacts)}`) - } - } - - // Only use filesystem fallback if no artifacts found from screenshotOnFail plugin - if (collectedArtifacts.length === 0 && test.state === 'failed') { - output.debug(`HTML Reporter: No artifacts from plugin, trying filesystem for test "${test.title}"`) - collectedArtifacts = collectScreenshotsFromFilesystem(test, test.id) - output.debug(`HTML Reporter: Collected ${collectedArtifacts.length} screenshots from filesystem for failed test "${test.title}"`) - if (collectedArtifacts.length > 0) { - output.debug(`HTML Reporter: Filesystem screenshots for "${test.title}": ${JSON.stringify(collectedArtifacts)}`) - } - } - - // Update test with processed artifacts - test.artifacts = collectedArtifacts - output.debug(`HTML Reporter: Final artifacts for "${test.title}": ${JSON.stringify(test.artifacts)}`) - }) - - // Calculate stats from our collected test data instead of using result.stats - const passedTests = reportData.tests.filter(t => t.state === 'passed').length - const failedTests = reportData.tests.filter(t => t.state === 'failed').length - // Combine pending and skipped tests (both represent tests that were not run) - const pendingTests = reportData.tests.filter(t => t.state === 'pending' || t.state === 'skipped').length - - // Calculate flaky tests (passed but had retries) - const flakyTests = reportData.tests.filter(t => t.state === 'passed' && t.retryAttempts > 0).length - - // Count total artifacts - const totalArtifacts = reportData.tests.reduce((sum, t) => sum + (t.artifacts?.length || 0), 0) - - // Populate failures from our collected test data with enhanced details - reportData.failures = reportData.tests - .filter(t => t.state === 'failed') - .map(t => { - const testName = t.title || 'Unknown Test' - // Try to get feature name from BDD, preserved titles (worker mode), or direct access - let featureName = t.feature?.name || t.parentTitle || t.suiteTitle || t.parent?.title || t.suite?.title || 'Unknown Feature' - if (featureName === 'Unknown Feature' && t.suite && t.suite.feature && t.suite.feature.name) { - featureName = t.suite.feature.name - } - - if (t.err) { - const errorMessage = t.err.message || t.err.toString() || 'Test failed' - const errorStack = t.err.stack || '' - const filePath = t.file || t.parent?.file || '' - - // Create enhanced failure object with test details - return { - testName: testName, - featureName: featureName, - message: errorMessage, - stack: errorStack, - filePath: filePath, - toString: () => `${testName} (${featureName})\n${errorMessage}\n${errorStack}`.trim(), - } - } - - return { - testName: testName, - featureName: featureName, - message: `Test failed: ${testName}`, - stack: '', - filePath: t.file || t.parent?.file || '', - toString: () => `${testName} (${featureName})\nTest failed: ${testName}`, - } - }) - - reportData.stats = { - tests: reportData.tests.length, - passes: passedTests, - failures: failedTests, - pending: pendingTests, - duration: reportData.duration, - failedHooks: result.stats?.failedHooks || 0, - flaky: flakyTests, - artifacts: totalArtifacts, - } - - // Debug logging for final stats - output.debug(`HTML Reporter: Calculated stats - Tests: ${reportData.stats.tests}, Passes: ${reportData.stats.passes}, Failures: ${reportData.stats.failures}`) - output.debug(`HTML Reporter: Collected ${reportData.tests.length} tests in reportData`) - output.debug(`HTML Reporter: Failures array has ${reportData.failures.length} items`) - output.debug(`HTML Reporter: Retries array has ${reportData.retries.length} items`) - output.debug(`HTML Reporter: testRetryAttempts Map size: ${testRetryAttempts.size}`) - - // Log retry attempts map contents - for (const [testId, attempts] of testRetryAttempts.entries()) { - output.debug(`HTML Reporter: testRetryAttempts - ${testId}: ${attempts} attempts`) - } - - reportData.tests.forEach(test => { - output.debug(`HTML Reporter: Test in reportData - ${test.title}, State: ${test.state}, Retries: ${test.retryAttempts}`) - }) - - // Check if running with workers - if (process.env.RUNS_WITH_WORKERS) { - // In worker mode, save results to a JSON file for later consolidation - const workerId = threadId - const jsonFileName = `worker-${workerId}-results.json` - const jsonPath = path.join(reportDir, jsonFileName) - - try { - // Always overwrite the file with the latest complete data from this worker - // This prevents double-counting when the event is triggered multiple times - fs.writeFileSync(jsonPath, safeJsonStringify(reportData)) - output.debug(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`) - } catch (error) { - output.debug(`HTML Reporter: Failed to write worker JSON: ${error.message}`) - } - return - } - - // Single process mode - generate report normally - try { - await generateHtmlReport(reportData, options) - } catch (error) { - output.print(`Failed to generate HTML report: ${error.message}`) - output.debug(`HTML Reporter error stack: ${error.stack}`) - } - - // Export stats if configured - if (options.exportStats) { - exportTestStats(reportData, options) - } - - // Save history if configured - if (options.keepHistory) { - saveTestHistory(reportData, options) - } - }) - - // Handle worker consolidation after all workers complete - event.dispatcher.on(event.workers.result, async result => { - if (process.env.RUNS_WITH_WORKERS) { - // Only run consolidation in main process - await consolidateWorkerJsonResults(options) - } - }) - - /** - * Safely serialize data to JSON, handling circular references - */ - function safeJsonStringify(data) { - const seen = new WeakSet() - return JSON.stringify( - data, - (key, value) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - // For error objects, try to extract useful information instead of "[Circular Reference]" - if (key === 'err' || key === 'error') { - return { - message: value.message || 'Error occurred', - stack: value.stack || '', - name: value.name || 'Error', - } - } - // Skip circular references for other objects - return undefined - } - seen.add(value) - - // Special handling for error objects to preserve important properties - if (value instanceof Error || (value.message && value.stack)) { - return { - message: value.message || '', - stack: value.stack || '', - name: value.name || 'Error', - toString: () => value.message || 'Error occurred', - } - } - } - return value - }, - 2, - ) - } - - function generateTestId(test) { - return crypto - .createHash('sha256') - .update(`${test.parent?.title || 'unknown'}_${test.title}`) - .digest('hex') - .substring(0, 8) - } - - function collectScreenshotsFromFilesystem(test, testId) { - const screenshots = [] - - try { - // Common screenshot locations to check - const possibleDirs = [ - reportDir, // Same as report directory - global.output_dir || './output', // Global output directory - path.resolve(global.codecept_dir || '.', 'output'), // Codecept output directory - path.resolve('.', 'output'), // Current directory output - path.resolve('.', '_output'), // Alternative output directory - path.resolve('output'), // Relative output directory - path.resolve('qa', 'output'), // QA project output directory - path.resolve('..', 'qa', 'output'), // Parent QA project output directory - ] - - // Use the exact same logic as screenshotOnFail plugin's testToFileName function - const originalTestName = test.title || 'test' - const originalFeatureName = test.parent?.title || 'feature' - - // Replicate testToFileName logic from lib/mocha/test.js - function replicateTestToFileName(testTitle) { - let fileName = testTitle - - // Slice to 100 characters first - fileName = fileName.slice(0, 100) - - // Handle data-driven tests: remove everything from '{' onwards (with 3 chars before) - if (fileName.indexOf('{') !== -1) { - fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim() - } - - // Apply clearString logic from utils.js - if (fileName.endsWith('.')) { - fileName = fileName.slice(0, -1) - } - fileName = fileName - .replace(/ /g, '_') - .replace(/"/g, "'") - .replace(/\//g, '_') - .replace(//g, ')') - .replace(/:/g, '_') - .replace(/\\/g, '_') - .replace(/\|/g, '_') - .replace(/\?/g, '.') - .replace(/\*/g, '^') - .replace(/'/g, '') - - // Final slice to 100 characters - return fileName.slice(0, 100) - } - - const testName = replicateTestToFileName(originalTestName) - const featureName = replicateTestToFileName(originalFeatureName) - - output.debug(`HTML Reporter: Original test title: "${originalTestName}"`) - output.debug(`HTML Reporter: CodeceptJS filename: "${testName}"`) - - // Generate possible screenshot names based on CodeceptJS patterns - const possibleNames = [ - `${testName}.failed.png`, // Primary CodeceptJS screenshotOnFail pattern - `${testName}.failed.jpg`, - `${featureName}_${testName}.failed.png`, - `${featureName}_${testName}.failed.jpg`, - `Test_${testName}.failed.png`, // Alternative pattern - `Test_${testName}.failed.jpg`, - `${testName}.png`, - `${testName}.jpg`, - `${featureName}_${testName}.png`, - `${featureName}_${testName}.jpg`, - `failed_${testName}.png`, - `failed_${testName}.jpg`, - `screenshot_${testId}.png`, - `screenshot_${testId}.jpg`, - 'screenshot.png', - 'screenshot.jpg', - 'failure.png', - 'failure.jpg', - ] - - output.debug(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`) - - // Search for screenshots in possible directories - for (const dir of possibleDirs) { - output.debug(`HTML Reporter: Checking directory: ${dir}`) - if (!fs.existsSync(dir)) { - output.debug(`HTML Reporter: Directory does not exist: ${dir}`) - continue - } - - try { - const files = fs.readdirSync(dir) - output.debug(`HTML Reporter: Found ${files.length} files in ${dir}`) - - // Look for exact matches first - for (const name of possibleNames) { - if (files.includes(name)) { - const fullPath = path.join(dir, name) - if (!screenshots.includes(fullPath)) { - screenshots.push(fullPath) - output.debug(`HTML Reporter: Found screenshot: ${fullPath}`) - } - } - } - - // Look for screenshot files that are specifically for this test - // Be more strict to avoid cross-test contamination - const screenshotFiles = files.filter(file => { - const lowerFile = file.toLowerCase() - const lowerTestName = testName.toLowerCase() - const lowerFeatureName = featureName.toLowerCase() - - return ( - file.match(/\.(png|jpg|jpeg|gif|webp|bmp)$/i) && - // Exact test name matches with .failed pattern (most specific) - (file === `${testName}.failed.png` || - file === `${testName}.failed.jpg` || - file === `${featureName}_${testName}.failed.png` || - file === `${featureName}_${testName}.failed.jpg` || - file === `Test_${testName}.failed.png` || - file === `Test_${testName}.failed.jpg` || - // Word boundary checks for .failed pattern - (lowerFile.includes('.failed.') && - (lowerFile.startsWith(lowerTestName + '.') || lowerFile.startsWith(lowerFeatureName + '_' + lowerTestName + '.') || lowerFile.startsWith('test_' + lowerTestName + '.')))) - ) - }) - - for (const file of screenshotFiles) { - const fullPath = path.join(dir, file) - if (!screenshots.includes(fullPath)) { - screenshots.push(fullPath) - output.debug(`HTML Reporter: Found related screenshot: ${fullPath}`) - } - } - } catch (error) { - // Ignore directory read errors - output.debug(`HTML Reporter: Could not read directory ${dir}: ${error.message}`) - } - } - } catch (error) { - output.debug(`HTML Reporter: Error collecting screenshots: ${error.message}`) - } - - return screenshots - } - - 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, // Already an ISO string - 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, // Already an ISO string - 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}`) - } - } - - /** - * Consolidates JSON reports from multiple workers into a single HTML report - */ - async function consolidateWorkerJsonResults(config) { - const jsonFiles = fs.readdirSync(reportDir).filter(file => file.startsWith('worker-') && file.endsWith('-results.json')) - - if (jsonFiles.length === 0) { - output.debug('HTML Reporter: No worker JSON results found to consolidate') - return - } - - output.debug(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`) - - // Initialize consolidated data structure - const consolidatedData = { - stats: { - tests: 0, - passes: 0, - failures: 0, - pending: 0, - skipped: 0, - duration: 0, - failedHooks: 0, - }, - tests: [], - failures: [], - hooks: [], - startTime: new Date(), - endTime: new Date(), - retries: [], - duration: 0, - } - - try { - // Process each worker's JSON file - for (const jsonFile of jsonFiles) { - const jsonPath = path.join(reportDir, jsonFile) - try { - const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) - - // Extract worker ID from filename (e.g., "worker-0-results.json" -> 0) - const workerIdMatch = jsonFile.match(/worker-(\d+)-results\.json/) - const workerIndex = workerIdMatch ? parseInt(workerIdMatch[1], 10) : undefined - - // Merge stats - if (workerData.stats) { - consolidatedData.stats.passes += workerData.stats.passes || 0 - consolidatedData.stats.failures += workerData.stats.failures || 0 - consolidatedData.stats.tests += workerData.stats.tests || 0 - consolidatedData.stats.pending += workerData.stats.pending || 0 - consolidatedData.stats.skipped += workerData.stats.skipped || 0 - consolidatedData.stats.duration += workerData.stats.duration || 0 - consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0 - } - - // Merge tests and add worker index - if (workerData.tests) { - const testsWithWorkerIndex = workerData.tests.map(test => ({ - ...test, - workerIndex: workerIndex, - })) - consolidatedData.tests.push(...testsWithWorkerIndex) - } - if (workerData.failures) consolidatedData.failures.push(...workerData.failures) - if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks) - if (workerData.retries) consolidatedData.retries.push(...workerData.retries) - - // Update timestamps - if (workerData.startTime) { - const workerStart = new Date(workerData.startTime).getTime() - const currentStart = new Date(consolidatedData.startTime).getTime() - if (workerStart < currentStart) { - consolidatedData.startTime = workerData.startTime - } - } - - if (workerData.endTime) { - const workerEnd = new Date(workerData.endTime).getTime() - const currentEnd = new Date(consolidatedData.endTime).getTime() - if (workerEnd > currentEnd) { - consolidatedData.endTime = workerData.endTime - } - } - - // Update duration - if (workerData.duration) { - consolidatedData.duration = Math.max(consolidatedData.duration, workerData.duration) - } - - // Clean up the worker JSON file - try { - fs.unlinkSync(jsonPath) - } catch (error) { - output.print(`Failed to delete worker JSON file ${jsonFile}: ${error.message}`) - } - } catch (error) { - output.print(`Failed to process worker JSON file ${jsonFile}: ${error.message}`) - } - } - - // Generate the final HTML report - generateHtmlReport(consolidatedData, config) - - // Export stats if configured - if (config.exportStats) { - exportTestStats(consolidatedData, config) - } - - // Save history if configured - if (config.keepHistory) { - saveTestHistory(consolidatedData, config) - } - - output.debug(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`) - } catch (error) { - output.debug(`HTML Reporter: Failed to consolidate worker reports: ${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')) // Show all available history - } - } catch (error) { - output.print(`Failed to load history for report: ${error.message}`) - } - - // Add current run to history for chart display (before saving to file) - const currentRun = { - timestamp: data.endTime, // Already an ISO string - duration: data.duration, - stats: data.stats, - retries: data.retries.length, - testCount: data.tests.length, - } - history.unshift(currentRun) - - // Limit history entries for chart display - if (history.length > config.maxHistoryEntries) { - history = history.slice(0, config.maxHistoryEntries) - } - } - - // Get system information - const systemInfo = await getMachineInfo() - - const html = template(getHtmlTemplate(), { - title: `CodeceptJS Test Report v${Codecept.version()}`, - timestamp: data.endTime, // Already an ISO string - duration: formatDuration(data.duration), - stats: JSON.stringify(data.stats), - history: JSON.stringify(history), - statsHtml: generateStatsHtml(data.stats), - testsHtml: generateTestsHtml(data.tests, config), - retriesHtml: config.showRetries ? generateRetriesHtml(data.retries) : '', - cssStyles: getCssStyles(), - jsScripts: getJsScripts(), - showRetries: config.showRetries ? 'block' : 'none', - showHistory: config.keepHistory && history.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 - const flaky = stats.flaky || 0 - const artifactCount = stats.artifacts || 0 - const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0.0' - const failRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0' - - return ` -
-
-

Total

- ${total} -
-
-

Passed

- ${passed} -
-
-

Failed

- ${failed} -
-
-

Skipped

- ${pending} -
-
-

Flaky

- ${flaky} -
-
-

Artifacts

- ${artifactCount} -
-
-
- Pass Rate: ${passRate}% - Fail Rate: ${failRate}% -
-
- - -
- ` - } - - function generateTestsHtml(tests, config) { - if (!tests || tests.length === 0) { - return '

No tests found.

' - } - - // Group tests by feature name - const grouped = {} - tests.forEach(test => { - const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature' - if (!grouped[feature]) grouped[feature] = [] - grouped[feature].push(test) - }) - - // Render each feature section - return Object.entries(grouped) - .map(([feature, tests]) => { - const featureId = feature.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() - return ` -
-

- ${escapeHtml(feature)} - -

-
- ${tests - .map(test => { - const statusClass = test.state || 'unknown' - 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, test.state === 'failed') : '' - 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, test.state) : '' - const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : '' - - // Worker badge - show worker index if test has worker info - const workerBadge = test.workerIndex !== undefined ? `Worker ${test.workerIndex}` : '' - - return ` -
-
- -
-

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

-
- ${workerBadge} - ${test.uid ? `${test.uid}` : ''} - ${formatDuration(test.duration)} - ${test.retryAttempts > 0 ? `${test.retryAttempts} retries` : ''} - ${test.isBdd ? 'Gherkin' : ''} -
-
-
-
- ${test.err ? `
${escapeHtml(getErrorMessage(test))}
` : ''} - ${featureDetails} - ${tags} - ${metadata} - ${retries} - ${notes} - ${hooks} - ${steps} - ${artifacts} -
-
- ` - }) - .join('')} -
-
- ` - }) - .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` - const location = hook.location ? `
Location: ${escapeHtml(hook.location)}
` : '' - const context = hook.context ? `
Test: ${escapeHtml(hook.context.testTitle || 'N/A')}, Suite: ${escapeHtml(hook.context.suiteTitle || 'N/A')}
` : '' - - return ` -
- -
- ${hookType}: ${hookTitle} - ${formatDuration(hook.duration)} - ${location} - ${context} - ${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:

- -
- ` - } - - 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, testState) { - // Enhanced retry history display showing whether test eventually passed or failed - const statusBadge = testState === 'passed' ? '✓ Eventually Passed' : '✗ Eventually Failed' - - return ` -
-

Retry History:

-
-
- Total retry attempts: ${retryAttempts} - ${statusBadge} -
-
- This test was retried ${retryAttempts} time${retryAttempts > 1 ? 's' : ''} before ${testState === 'passed' ? 'passing' : 'failing'}. -
-
-
- ` - } - - function generateArtifactsHtml(artifacts, isFailedTest = false) { - if (!artifacts || artifacts.length === 0) { - output.debug(`HTML Reporter: No artifacts found for test`) - return '' - } - - output.debug(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`) - output.debug(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`) - - // Separate screenshots from other artifacts - const screenshots = [] - const otherArtifacts = [] - - artifacts.forEach(artifact => { - output.debug(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`) - - // Handle different artifact formats - let artifactPath = artifact - if (typeof artifact === 'object' && artifact.path) { - artifactPath = artifact.path - } else if (typeof artifact === 'object' && artifact.file) { - artifactPath = artifact.file - } else if (typeof artifact === 'object' && artifact.src) { - artifactPath = artifact.src - } - - // Check if it's a screenshot file - if (typeof artifactPath === 'string' && artifactPath.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i)) { - screenshots.push(artifactPath) - output.debug(`HTML Reporter: Found screenshot: ${artifactPath}`) - } else { - otherArtifacts.push(artifact) - output.debug(`HTML Reporter: Found other artifact: ${artifact}`) - } - }) - - output.debug(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`) - - let artifactsHtml = '' - - // For failed tests, prominently display screenshots - if (isFailedTest && screenshots.length > 0) { - const screenshotsHtml = screenshots - .map(screenshot => { - let relativePath = path.relative(reportDir, screenshot) - const filename = path.basename(screenshot) - - // If relative path goes up directories, try to find the file in common locations - if (relativePath.startsWith('..')) { - // Try to find screenshot relative to output directory - const outputRelativePath = path.relative(reportDir, path.resolve(screenshot)) - if (!outputRelativePath.startsWith('..')) { - relativePath = outputRelativePath - } else { - // Use just the filename if file is in same directory as report - const sameDir = path.join(reportDir, filename) - if (fs.existsSync(sameDir)) { - relativePath = filename - } else { - // Keep original path as fallback - relativePath = screenshot - } - } - } - - output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`) - - return ` -
-
- 📸 ${escapeHtml(filename)} -
- Test failure screenshot -
- ` - }) - .join('') - - artifactsHtml += ` -
-

Screenshots:

-
${screenshotsHtml}
-
- ` - } else if (screenshots.length > 0) { - // For non-failed tests, display screenshots normally - const screenshotsHtml = screenshots - .map(screenshot => { - let relativePath = path.relative(reportDir, screenshot) - const filename = path.basename(screenshot) - - // If relative path goes up directories, try to find the file in common locations - if (relativePath.startsWith('..')) { - // Try to find screenshot relative to output directory - const outputRelativePath = path.relative(reportDir, path.resolve(screenshot)) - if (!outputRelativePath.startsWith('..')) { - relativePath = outputRelativePath - } else { - // Use just the filename if file is in same directory as report - const sameDir = path.join(reportDir, filename) - if (fs.existsSync(sameDir)) { - relativePath = filename - } else { - // Keep original path as fallback - relativePath = screenshot - } - } - } - - output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`) - return `Screenshot` - }) - .join('') - - artifactsHtml += ` -
-

Screenshots:

-
${screenshotsHtml}
-
- ` - } - - // Display other artifacts if any - if (otherArtifacts.length > 0) { - const otherArtifactsHtml = otherArtifacts.map(artifact => `
${escapeHtml(artifact.toString())}
`).join('') - - artifactsHtml += ` -
-

Other Artifacts:

-
${otherArtifactsHtml}
-
- ` - } - - return artifactsHtml - ? ` -
- ${artifactsHtml} -
- ` - : '' - } - - function generateFailuresHtml(failures) { - if (!failures || failures.length === 0) { - return '

No failures.

' - } - - return failures - .map((failure, index) => { - // Helper function to safely extract string values - const safeString = value => { - if (!value) return '' - if (typeof value === 'string') return value - if (typeof value === 'object' && value.toString) { - const str = value.toString() - return str === '[object Object]' ? '' : str - } - return String(value) - } - - if (typeof failure === 'object' && failure !== null) { - // Enhanced failure object with test details - console.log('this is failure', failure) - const testName = safeString(failure.testName) || 'Unknown Test' - const featureName = safeString(failure.featureName) || 'Unknown Feature' - let message = safeString(failure.message) || 'Test failed' - const stack = safeString(failure.stack) || '' - const filePath = safeString(failure.filePath) || '' - - // If message is still "[object Object]", try to extract from the failure object itself - if (message === '[object Object]' || message === '') { - if (failure.err && failure.err.message) { - message = safeString(failure.err.message) - } else if (failure.error && failure.error.message) { - message = safeString(failure.error.message) - } else if (failure.toString && typeof failure.toString === 'function') { - const str = failure.toString() - message = str === '[object Object]' ? 'Test failed' : str - } else { - message = 'Test failed' - } - } - - return ` -
-

Failure ${index + 1}: ${escapeHtml(testName)}

-
- Feature: ${escapeHtml(featureName)} - ${filePath ? `File: ${escapeHtml(filePath)}` : ''} -
-
- Error: ${escapeHtml(message)} -
- ${stack ? `
${escapeHtml(stack.replace(/\x1b\[[0-9;]*m/g, ''))}
` : ''} -
- ` - } else { - // Fallback for simple string failures - const failureText = safeString(failure).replace(/\x1b\[[0-9;]*m/g, '') || 'Test failed' - 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(text) { - if (!text) return '' - return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') - } - - function getErrorMessage(test) { - if (!test) return 'Test failed' - - // Helper function to safely extract string from potentially circular objects - const safeExtract = (obj, prop) => { - try { - if (!obj || typeof obj !== 'object') return '' - const value = obj[prop] - if (typeof value === 'string') return value - if (value && typeof value.toString === 'function') { - const str = value.toString() - return str === '[object Object]' ? '' : str - } - return '' - } catch (e) { - return '' - } - } - - // Helper function to safely stringify objects avoiding circular references - const safeStringify = obj => { - try { - if (!obj) return '' - if (typeof obj === 'string') return obj - - // Try to get message property first - if (obj.message && typeof obj.message === 'string') { - return obj.message - } - - // For error objects, extract key properties manually - if (obj instanceof Error || (obj.name && obj.message)) { - return obj.message || obj.toString() || 'Error occurred' - } - - // For other objects, try toString first - if (obj.toString && typeof obj.toString === 'function') { - const str = obj.toString() - if (str !== '[object Object]' && !str.includes('[Circular Reference]')) { - return str - } - } - - // Last resort: extract message-like properties - if (obj.message) return obj.message - if (obj.description) return obj.description - if (obj.text) return obj.text - - return 'Error occurred' - } catch (e) { - return 'Error occurred' - } - } - - let errorMessage = '' - let errorStack = '' - - // Primary error source - if (test.err) { - errorMessage = safeExtract(test.err, 'message') || safeStringify(test.err) - errorStack = safeExtract(test.err, 'stack') - } - - // Alternative error sources for different test frameworks - if (!errorMessage && test.error) { - errorMessage = safeExtract(test.error, 'message') || safeStringify(test.error) - errorStack = safeExtract(test.error, 'stack') - } - - // Check for nested error in parent - if (!errorMessage && test.parent && test.parent.err) { - errorMessage = safeExtract(test.parent.err, 'message') || safeStringify(test.parent.err) - errorStack = safeExtract(test.parent.err, 'stack') - } - - // Check for error details array (some frameworks use this) - if (!errorMessage && test.err && test.err.details && Array.isArray(test.err.details)) { - errorMessage = test.err.details - .map(item => safeExtract(item, 'message') || safeStringify(item)) - .filter(msg => msg && msg !== '[Circular]') - .join(' ') - } - - // Fallback to test title if no error message found - if (!errorMessage || errorMessage === '[Circular]') { - errorMessage = `Test failed: ${test.title || 'Unknown test'}` - } - - // Clean ANSI escape codes and remove circular reference markers - const cleanMessage = (errorMessage || '') - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\[Circular\]/g, '') - .replace(/\s+/g, ' ') - .trim() - - const cleanStack = (errorStack || '') - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\[Circular\]/g, '') - .trim() - - // Return combined error information - if (cleanStack && cleanStack !== cleanMessage && !cleanMessage.includes(cleanStack)) { - return `${cleanMessage}\n\nStack trace:\n${cleanStack}` - } - - return cleanMessage - } - - 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 Performance Analysis

-
-
-

⏱️ Longest Running Tests

-
-
-
-

⚡ Fastest Tests

-
-
-
-
- -
-

Test Execution History

-
-
-
- -
-
- -
-

Filters

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
- -
-

Test Results

-
- {{testsHtml}} -
-
- - - -
- - - - - - - - - ` - } - - 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, .retries-section, .filters-section, .history-section, .system-info-section, .test-performance-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, .retries-section h2, .filters-section h2, .history-section h2, .test-performance-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.flaky { background: #e67e22; } -.stat-card.artifacts { background: #9b59b6; } - -.metrics-summary { - display: flex; - justify-content: center; - gap: 2rem; - padding: 1rem; - background: #f8f9fa; - border-radius: 6px; - margin: 1rem 0; - font-size: 1rem; -} - -.metrics-summary span { - color: #34495e; -} - -.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; -} - -.feature-group { - margin-bottom: 2.5rem; - border: 2px solid #3498db; - border-radius: 12px; - overflow: hidden; - background: white; - box-shadow: 0 4px 8px rgba(0,0,0,0.1); -} - -.feature-group-title { - background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); - color: white; - padding: 1.2rem 1.5rem; - margin: 0; - font-size: 1.4rem; - font-weight: 600; - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - transition: all 0.3s ease; - user-select: none; -} - -.feature-group-title:hover { - background: linear-gradient(135deg, #2980b9 0%, #21618c 100%); -} - -.feature-group-title .toggle-icon { - font-size: 1.2rem; - transition: transform 0.3s ease; -} - -.feature-group-title .toggle-icon.rotated { - transform: rotate(180deg); -} - -.feature-tests { - padding: 0; - transition: max-height 0.3s ease, opacity 0.3s ease; -} - -.feature-tests.collapsed { - max-height: 0; - opacity: 0; - overflow: hidden; -} - -.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; -} - -.worker-badge { - background: #16a085; - color: white; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.7rem; - font-weight: bold; -} - -/* Different colors for each worker index */ -.worker-badge.worker-0 { - background: #3498db; /* Blue */ -} - -.worker-badge.worker-1 { - background: #e74c3c; /* Red */ -} - -.worker-badge.worker-2 { - background: #2ecc71; /* Green */ -} - -.worker-badge.worker-3 { - background: #f39c12; /* Orange */ -} - -.worker-badge.worker-4 { - background: #9b59b6; /* Purple */ -} - -.worker-badge.worker-5 { - background: #1abc9c; /* Turquoise */ -} - -.worker-badge.worker-6 { - background: #e67e22; /* Carrot */ -} - -.worker-badge.worker-7 { - background: #34495e; /* Dark Blue-Gray */ -} - -.worker-badge.worker-8 { - background: #16a085; /* Teal */ -} - -.worker-badge.worker-9 { - background: #c0392b; /* Dark Red */ -} - -.test-duration { - font-size: 0.85rem; - font-weight: 600; - color: #2c3e50; - background: #ecf0f1; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-family: 'Monaco', 'Courier New', monospace; -} - -.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: flex-start; - padding: 0.75rem; - border: 1px solid #ecf0f1; - border-radius: 4px; - margin-bottom: 0.5rem; - background: #fafafa; -} - -.hook-item:last-child { - margin-bottom: 0; -} - -.hook-status { - margin-right: 0.75rem; - flex-shrink: 0; - margin-top: 0.2rem; -} - -.hook-status.passed { color: #27ae60; } -.hook-status.failed { color: #e74c3c; } - -.hook-content { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.hook-title { - font-family: 'Courier New', monospace; - font-size: 0.9rem; - font-weight: bold; - color: #2c3e50; -} - -.hook-duration { - font-size: 0.8rem; - color: #7f8c8d; -} - -.hook-location, .hook-context { - font-size: 0.8rem; - color: #6c757d; - font-style: italic; -} - -.hook-error { - 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: flex-start; - padding: 0.5rem 0; - border-bottom: 1px solid #ecf0f1; - word-wrap: break-word; - overflow-wrap: break-word; - min-height: 2rem; -} - -.step-item:last-child { - border-bottom: none; -} - -.step-status { - margin-right: 0.5rem; - flex-shrink: 0; - margin-top: 0.2rem; -} - -.step-status.success { color: #27ae60; } -.step-status.failed { color: #e74c3c; } - -.step-title { - flex: 1; - font-family: 'Courier New', monospace; - font-size: 0.9rem; - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.4; - margin-right: 0.5rem; - min-width: 0; -} - -.step-duration { - font-size: 0.8rem; - color: #7f8c8d; - flex-shrink: 0; - margin-top: 0.2rem; -} - -.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; -} - -/* Enhanced screenshot styles for failed tests */ -.screenshots-section { - margin-top: 1rem; -} - -.screenshots-section h4 { - color: #e74c3c; - margin-bottom: 0.75rem; - font-size: 1rem; - font-weight: 600; -} - -.screenshots-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.screenshot-container { - border: 2px solid #e74c3c; - border-radius: 8px; - overflow: hidden; - background: white; - box-shadow: 0 4px 8px rgba(231, 76, 60, 0.1); -} - -.screenshot-header { - background: #e74c3c; - color: white; - padding: 0.5rem 1rem; - font-size: 0.9rem; - font-weight: 500; -} - -.screenshot-label { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.failure-screenshot { - width: 100%; - max-width: 100%; - height: auto; - display: block; - cursor: pointer; - transition: opacity 0.2s; -} - -.failure-screenshot:hover { - opacity: 0.9; -} - -.other-artifacts-section { - margin-top: 1rem; -} - -/* 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-section { - margin-top: 1rem; -} - -.retry-info { - padding: 1rem; - background: #fff9e6; - border-radius: 4px; - border-left: 4px solid #f39c12; -} - -.retry-summary { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 0.5rem; -} - -.retry-count { - color: #d68910; - font-weight: 500; -} - -.retry-status-badge { - padding: 0.25rem 0.75rem; - border-radius: 4px; - font-size: 0.85rem; - font-weight: bold; -} - -.retry-status-badge.passed { - background: #27ae60; - color: white; -} - -.retry-status-badge.failed { - background: #e74c3c; - color: white; -} - -.retry-description { - font-size: 0.9rem; - color: #6c757d; - font-style: italic; -} - -/* 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-stats { - padding: 1.5rem; - background: #f8f9fa; - border-bottom: 1px solid #e9ecef; -} - -.history-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; -} - -.history-stat-item { - background: white; - padding: 1rem; - border-radius: 6px; - border-left: 4px solid #3498db; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.history-stat-item h4 { - margin: 0 0 0.5rem 0; - font-size: 0.9rem; - color: #7f8c8d; - text-transform: uppercase; -} - -.history-stat-item .value { - font-size: 1.5rem; - font-weight: bold; - color: #2c3e50; -} - -.history-timeline { - padding: 1.5rem; - background: white; -} - -.timeline-item { - display: flex; - align-items: center; - padding: 0.75rem; - border-left: 3px solid #3498db; - margin-left: 1rem; - margin-bottom: 0.5rem; - background: #f8f9fa; - border-radius: 0 6px 6px 0; - transition: all 0.2s; -} - -.timeline-item:hover { - background: #e9ecef; - transform: translateX(4px); -} - -.timeline-time { - min-width: 150px; - font-weight: 600; - color: #2c3e50; - font-family: 'Courier New', monospace; -} - -.timeline-result { - flex: 1; - display: flex; - gap: 1rem; - align-items: center; -} - -.timeline-badge { - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.85rem; - font-weight: 600; -} - -.timeline-badge.success { - background: #d4edda; - color: #155724; -} - -.timeline-badge.failure { - background: #f8d7da; - color: #721c24; -} - -.history-chart-container { - padding: 2rem 1rem; - display: flex; - justify-content: center; -} - -#historyChart { - max-width: 100%; - height: auto; -} - -/* Test Performance Section */ -.performance-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 2rem; - padding: 1.5rem; -} - -.performance-group h3 { - margin: 0 0 1rem 0; - color: #2c3e50; - font-size: 1.1rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid #3498db; -} - -.performance-list { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.performance-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - background: #f8f9fa; - border-radius: 6px; - border-left: 4px solid #3498db; - transition: all 0.2s; -} - -.performance-item:hover { - background: #e9ecef; - transform: translateX(4px); -} - -.performance-item:nth-child(1) .performance-rank { - background: #f39c12; - color: white; -} - -.performance-item:nth-child(2) .performance-rank { - background: #95a5a6; - color: white; -} - -.performance-item:nth-child(3) .performance-rank { - background: #cd7f32; - color: white; -} - -.performance-rank { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: #3498db; - color: white; - border-radius: 50%; - font-weight: bold; - font-size: 0.9rem; - margin-right: 1rem; - flex-shrink: 0; -} - -.performance-name { - flex: 1; - font-weight: 500; - color: #2c3e50; -} - -.performance-duration { - font-weight: 600; - color: #7f8c8d; - font-family: 'Courier New', monospace; - font-size: 0.9rem; -} - -/* 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; - word-wrap: break-word; - overflow-wrap: break-word; - min-height: 2rem; -} - -.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; - flex-shrink: 0; -} - -.bdd-step-text { - flex: 1; - color: #2c3e50; - margin-right: 0.5rem; - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.4; - min-width: 0; -} - -.step-comment { - width: 100%; - margin-top: 0.5rem; - padding: 0.5rem; - background: #f8f9fa; - border-left: 3px solid #8e44ad; - font-style: italic; - color: #6c757d; - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.4; -} - -@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 ` -// Go to Top button -function scrollToTop() { - window.scrollTo({ top: 0, behavior: 'smooth' }); -} - -function toggleFeatureGroup(featureId) { - const featureTests = document.getElementById('feature-' + featureId); - const titleElement = featureTests.previousElementSibling; - const icon = titleElement.querySelector('.toggle-icon'); - - if (featureTests.classList.contains('collapsed')) { - featureTests.classList.remove('collapsed'); - icon.classList.remove('rotated'); - } else { - featureTests.classList.add('collapsed'); - icon.classList.add('rotated'); - } -} - -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 - - // Calculate percentages - const passedPercent = Math.round((data.passed / total) * 100); - const failedPercent = Math.round((data.failed / total) * 100); - const pendingPercent = Math.round((data.pending / total) * 100); - - // 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(); - - // Add percentage text on segment if significant enough - if (passedPercent >= 10) { - const textAngle = currentAngle + angle / 2; - const textRadius = radius * 0.7; - const textX = centerX + Math.cos(textAngle) * textRadius; - const textY = centerY + Math.sin(textAngle) * textRadius; - - ctx.fillStyle = '#fff'; - ctx.font = 'bold 14px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(passedPercent + '%', textX, textY); - } - - 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(); - - // Add percentage text on segment if significant enough - if (failedPercent >= 10) { - const textAngle = currentAngle + angle / 2; - const textRadius = radius * 0.7; - const textX = centerX + Math.cos(textAngle) * textRadius; - const textY = centerY + Math.sin(textAngle) * textRadius; - - ctx.fillStyle = '#fff'; - ctx.font = 'bold 14px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(failedPercent + '%', textX, textY); - } - - 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 percentage text on segment if significant enough - if (pendingPercent >= 10) { - const textAngle = currentAngle + angle / 2; - const textRadius = radius * 0.7; - const textX = centerX + Math.cos(textAngle) * textRadius; - const textY = centerY + Math.sin(textAngle) * textRadius; - - ctx.fillStyle = '#fff'; - ctx.font = 'bold 14px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(pendingPercent + '%', textX, textY); - } - } - - // Add legend with percentages - const legendY = centerY + radius + 40; - ctx.font = '14px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'alphabetic'; - - let legendX = centerX - 150; - - // Passed legend - ctx.fillStyle = '#27ae60'; - ctx.fillRect(legendX, legendY, 15, 15); - ctx.fillStyle = '#333'; - ctx.fillText('Passed (' + data.passed + ' - ' + passedPercent + '%)', legendX + 20, legendY + 12); - - // Failed legend - legendX += 130; - ctx.fillStyle = '#e74c3c'; - ctx.fillRect(legendX, legendY, 15, 15); - ctx.fillStyle = '#333'; - ctx.fillText('Failed (' + data.failed + ' - ' + failedPercent + '%)', legendX + 20, legendY + 12); - - // Pending legend - if (data.pending > 0) { - legendX += 120; - ctx.fillStyle = '#f39c12'; - ctx.fillRect(legendX, legendY, 15, 15); - ctx.fillStyle = '#333'; - ctx.fillText('Pending (' + data.pending + ' - ' + pendingPercent + '%)', legendX + 20, legendY + 12); - } -} - -// Draw history chart -function drawHistoryChart() { - const canvas = document.getElementById('historyChart'); - - if (!canvas || !window.testData || !window.testData.history || window.testData.history.length === 0) { - return; - } - - const ctx = canvas.getContext('2d'); - const history = window.testData.history.slice().reverse(); // Most recent last - console.log('History chart - Total data points:', window.testData.history.length); - console.log('History chart - Processing points:', history.length); - console.log('History chart - Raw history data:', window.testData.history); - console.log('History chart - Reversed history:', history); - - const padding = 60; - const bottomPadding = 80; // Extra space for timestamps - const chartWidth = canvas.width - 2 * padding; - const chartHeight = canvas.height - padding - bottomPadding; - - // Clear canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Calculate success rates and max values - const dataPoints = history.map((run, index) => { - const total = run.stats.tests || 0; - const passed = run.stats.passes || 0; - const failed = run.stats.failures || 0; - const successRate = total > 0 ? (passed / total) * 100 : 0; - const timestamp = new Date(run.timestamp); - - return { - index, - timestamp, - total, - passed, - failed, - successRate, - duration: run.duration || 0, - retries: run.retries || 0 - }; - }); - - console.log('History chart - Data points created:', dataPoints.length); - console.log('History chart - Data points:', dataPoints); - - const maxTests = Math.max(...dataPoints.map(d => d.total)); - const maxSuccessRate = 100; - - if (maxTests === 0) return; - - // Draw background - ctx.fillStyle = '#fafafa'; - ctx.fillRect(padding, padding, chartWidth, chartHeight); - - // Draw axes - ctx.strokeStyle = '#333'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(padding, padding); - ctx.lineTo(padding, padding + chartHeight); - ctx.lineTo(padding + chartWidth, padding + chartHeight); - ctx.stroke(); - - // Draw grid lines - ctx.strokeStyle = '#e0e0e0'; - ctx.lineWidth = 1; - for (let i = 1; i <= 4; i++) { - const y = padding + (chartHeight * i / 4); - ctx.beginPath(); - ctx.moveTo(padding, y); - ctx.lineTo(padding + chartWidth, y); - ctx.stroke(); - } - - // Calculate positions - const stepX = dataPoints.length > 1 ? chartWidth / (dataPoints.length - 1) : chartWidth / 2; - - // Draw success rate area chart - ctx.fillStyle = 'rgba(39, 174, 96, 0.1)'; - ctx.strokeStyle = '#27ae60'; - ctx.lineWidth = 3; - ctx.beginPath(); - - dataPoints.forEach((point, index) => { - const x = dataPoints.length === 1 ? padding + chartWidth / 2 : padding + (index * stepX); - const y = padding + chartHeight - (point.successRate / maxSuccessRate) * chartHeight; - - if (index === 0) { - ctx.moveTo(x, padding + chartHeight); - ctx.lineTo(x, y); - } else { - ctx.lineTo(x, y); - } - - point.x = x; - point.y = y; - }); - - // Close the area - if (dataPoints.length > 0) { - const lastPoint = dataPoints[dataPoints.length - 1]; - ctx.lineTo(lastPoint.x, padding + chartHeight); - ctx.closePath(); - ctx.fill(); - } - - // Draw success rate line - ctx.strokeStyle = '#27ae60'; - ctx.lineWidth = 3; - ctx.beginPath(); - dataPoints.forEach((point, index) => { - if (index === 0) { - ctx.moveTo(point.x, point.y); - } else { - ctx.lineTo(point.x, point.y); - } - }); - ctx.stroke(); - - // Draw data points with enhanced styling - dataPoints.forEach(point => { - // Outer ring based on status - const ringColor = point.failed > 0 ? '#e74c3c' : '#27ae60'; - ctx.strokeStyle = ringColor; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI); - ctx.stroke(); - - // Inner circle - ctx.fillStyle = point.failed > 0 ? '#e74c3c' : '#27ae60'; - ctx.beginPath(); - ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); - ctx.fill(); - - // White center dot - ctx.fillStyle = '#fff'; - ctx.beginPath(); - ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI); - ctx.fill(); - }); - - // Y-axis labels (Success Rate %) - ctx.fillStyle = '#666'; - ctx.font = '11px Arial'; - ctx.textAlign = 'right'; - for (let i = 0; i <= 4; i++) { - const value = Math.round((maxSuccessRate * i) / 4); - const y = padding + chartHeight - (chartHeight * i / 4); - ctx.fillText(value + '%', padding - 10, y + 4); - } - - // X-axis labels (Timestamps) - ctx.textAlign = 'center'; - ctx.font = '10px Arial'; - dataPoints.forEach((point, index) => { - const timeStr = point.timestamp.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: false - }); - const dateStr = point.timestamp.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric' - }); - - console.log('Drawing label ' + index + ': ' + timeStr + ' at x=' + point.x); - ctx.fillText(timeStr, point.x, padding + chartHeight + 15); - ctx.fillText(dateStr, point.x, padding + chartHeight + 30); - }); - - // Enhanced legend with statistics - const legendY = 25; - ctx.font = '12px Arial'; - ctx.textAlign = 'left'; - - // Success rate legend - ctx.fillStyle = '#27ae60'; - ctx.fillRect(padding + 20, legendY, 15, 15); - ctx.fillStyle = '#333'; - ctx.fillText('Success Rate', padding + 40, legendY + 12); - - // Current stats - if (dataPoints.length > 0) { - const latest = dataPoints[dataPoints.length - 1]; - const trend = dataPoints.length > 1 ? - (latest.successRate - dataPoints[dataPoints.length - 2].successRate) : 0; - const trendIcon = trend > 0 ? '↗' : trend < 0 ? '↘' : '→'; - const trendColor = trend > 0 ? '#27ae60' : trend < 0 ? '#e74c3c' : '#666'; - - ctx.fillStyle = '#666'; - ctx.fillText('Latest: ' + latest.successRate.toFixed(1) + '%', padding + 150, legendY + 12); - - ctx.fillStyle = trendColor; - ctx.fillText(trendIcon + ' ' + Math.abs(trend).toFixed(1) + '%', padding + 240, legendY + 12); - } - - // Chart title - ctx.fillStyle = '#333'; - ctx.font = 'bold 14px Arial'; - ctx.textAlign = 'center'; - ctx.fillText('Test Success Rate History', canvas.width / 2, 20); -} - -// Initialize charts and filters -document.addEventListener('DOMContentLoaded', function() { - - // Draw charts - drawPieChart(); - drawHistoryChart(); - renderTestPerformance(); - renderHistoryTimeline(); - - // Add Go to Top button - const goTopBtn = document.createElement('button'); - goTopBtn.innerText = '↑ Top'; - goTopBtn.id = 'goTopBtn'; - goTopBtn.style.position = 'fixed'; - goTopBtn.style.bottom = '30px'; - goTopBtn.style.right = '30px'; - goTopBtn.style.zIndex = '9999'; - goTopBtn.style.padding = '12px 18px'; - goTopBtn.style.borderRadius = '50%'; - goTopBtn.style.background = '#27ae60'; - goTopBtn.style.color = '#fff'; - goTopBtn.style.fontSize = '20px'; - goTopBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; - goTopBtn.style.cursor = 'pointer'; - goTopBtn.onclick = scrollToTop; - document.body.appendChild(goTopBtn); - - // 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); -}); - -// Render test performance analysis -function renderTestPerformance() { - const tests = Array.from(document.querySelectorAll('.test-item')); - const testsWithDuration = tests.map(testEl => { - const title = testEl.querySelector('.test-title')?.textContent || 'Unknown'; - const durationText = testEl.querySelector('.test-duration')?.textContent || '0ms'; - const durationMs = parseDuration(durationText); - const status = testEl.dataset.status; - return { title, duration: durationMs, durationText, status }; - }); // Don't filter out 0ms tests - - // Sort by duration - const longest = [...testsWithDuration].sort((a, b) => b.duration - a.duration).slice(0, 5); - const fastest = [...testsWithDuration].sort((a, b) => a.duration - b.duration).slice(0, 5); - - // Render longest tests - const longestContainer = document.getElementById('longestTests'); - if (longestContainer && longest.length > 0) { - longestContainer.innerHTML = longest.map((test, index) => \` -
- \${index + 1} - \${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title} - \${test.durationText} -
- \`).join(''); - } else if (longestContainer) { - longestContainer.innerHTML = '

No test data available

'; - } - - // Render fastest tests - const fastestContainer = document.getElementById('fastestTests'); - if (fastestContainer && fastest.length > 0) { - fastestContainer.innerHTML = fastest.map((test, index) => \` -
- \${index + 1} - \${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title} - \${test.durationText} -
- \`).join(''); - } else if (fastestContainer) { - fastestContainer.innerHTML = '

No test data available

'; - } -} - -// Render history timeline -function renderHistoryTimeline() { - if (!window.testData || !window.testData.history || window.testData.history.length === 0) { - return; - } - - const history = window.testData.history.slice().reverse(); // Most recent last - - // Render stats - const statsContainer = document.getElementById('historyStats'); - if (statsContainer) { - const totalRuns = history.length; - const avgDuration = history.reduce((sum, run) => sum + (run.duration || 0), 0) / totalRuns; - const avgTests = Math.round(history.reduce((sum, run) => sum + (run.stats.tests || 0), 0) / totalRuns); - const avgPassRate = history.reduce((sum, run) => { - const total = run.stats.tests || 0; - const passed = run.stats.passes || 0; - return sum + (total > 0 ? (passed / total) * 100 : 0); - }, 0) / totalRuns; - - statsContainer.innerHTML = \` -
-
-

Total Runs

-
\${totalRuns}
-
-
-

Avg Duration

-
\${formatDuration(avgDuration)}
-
-
-

Avg Tests

-
\${avgTests}
-
-
-

Avg Pass Rate

-
\${avgPassRate.toFixed(1)}%
-
-
- \`; - } - - // Render timeline - const timelineContainer = document.getElementById('historyTimeline'); - if (timelineContainer) { - const recentHistory = history.slice(-10).reverse(); // Last 10 runs, most recent first - timelineContainer.innerHTML = '

Recent Execution Timeline

' + - recentHistory.map(run => { - const timestamp = new Date(run.timestamp); - const timeStr = timestamp.toLocaleString(); - const total = run.stats.tests || 0; - const passed = run.stats.passes || 0; - const failed = run.stats.failures || 0; - const badgeClass = failed > 0 ? 'failure' : 'success'; - const badgeText = failed > 0 ? \`\${failed} Failed\` : \`All Passed\`; - - return \` -
-
\${timeStr}
-
- \${badgeText} - \${passed}/\${total} passed - · - \${formatDuration(run.duration || 0)} -
-
- \`; - }).join(''); - } -} - -// Helper to parse duration text to milliseconds -function parseDuration(durationText) { - if (!durationText) return 0; - const match = durationText.match(/(\\d+(?:\\.\\d+)?)(ms|s|m)/); - if (!match) return 0; - const value = parseFloat(match[1]); - const unit = match[2]; - if (unit === 'ms') return value; - if (unit === 's') return value * 1000; - if (unit === 'm') return value * 60000; - return 0; -} - -// Helper to format duration -function formatDuration(ms) { - if (ms < 1000) return Math.round(ms) + 'ms'; - if (ms < 60000) return (ms / 1000).toFixed(2) + 's'; - return (ms / 60000).toFixed(2) + 'm'; -} - ` - } -} diff --git a/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js b/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js deleted file mode 100644 index 6f75dd885..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/artifacts_test.js +++ /dev/null @@ -1,20 +0,0 @@ -import container from '../../../../../lib/container.js' - -Feature('HTML Reporter with Artifacts Test') - -Scenario('test with artifacts', async ({ I }) => { - I.amInPath('.') - I.seeFile('codecept.conf.js') - - // Simulate adding test artifacts - 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.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.cjs deleted file mode 100644 index faf46f210..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.cjs +++ /dev/null @@ -1,31 +0,0 @@ -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-bdd.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.js deleted file mode 100644 index faf46f210..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-bdd.conf.js +++ /dev/null @@ -1,31 +0,0 @@ -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.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.cjs deleted file mode 100644 index 8949ea5a3..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.cjs +++ /dev/null @@ -1,27 +0,0 @@ -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-history.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.js deleted file mode 100644 index 8949ea5a3..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-history.conf.js +++ /dev/null @@ -1,27 +0,0 @@ -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-retries.conf.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.cjs deleted file mode 100644 index f2ee21035..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure') - -setHeadlessWhen(process.env.HEADLESS) -setWindowSize(1600, 1200) - -exports.config = { - tests: './retry_test.js', - output: './output', - helpers: { - FileSystem: {}, - }, - plugins: { - htmlReporter: { - enabled: true, - output: './output', - reportFileName: 'retry-report.html', - includeArtifacts: true, - showSteps: true, - showRetries: true, - }, - retryFailedStep: { - enabled: true, - retries: 2, - }, - }, - name: 'html-reporter-plugin retry tests', -} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js deleted file mode 100644 index f2ee21035..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js +++ /dev/null @@ -1,27 +0,0 @@ -const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure') - -setHeadlessWhen(process.env.HEADLESS) -setWindowSize(1600, 1200) - -exports.config = { - tests: './retry_test.js', - output: './output', - helpers: { - FileSystem: {}, - }, - plugins: { - htmlReporter: { - enabled: true, - output: './output', - reportFileName: 'retry-report.html', - includeArtifacts: true, - showSteps: true, - showRetries: true, - }, - retryFailedStep: { - enabled: true, - retries: 2, - }, - }, - name: 'html-reporter-plugin retry tests', -} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.cjs deleted file mode 100644 index a64c5c2d3..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.cjs +++ /dev/null @@ -1,26 +0,0 @@ -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-with-stats.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.js deleted file mode 100644 index a64c5c2d3..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-stats.conf.js +++ /dev/null @@ -1,26 +0,0 @@ -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-workers.conf.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.cjs deleted file mode 100644 index 9b5043f08..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.cjs +++ /dev/null @@ -1,34 +0,0 @@ -const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure') - -setHeadlessWhen(process.env.HEADLESS) -setWindowSize(1600, 1200) - -exports.config = { - tests: './*_test.js', - output: './output', - helpers: { - FileSystem: {}, - }, - plugins: { - htmlReporter: { - enabled: true, - output: './output', - reportFileName: 'worker-report.html', - includeArtifacts: true, - showSteps: true, - showSkipped: true, - showMetadata: true, - showTags: true, - showRetries: true, - exportStats: false, - keepHistory: false, - }, - }, - multiple: { - parallel: { - chunks: 2, - browsers: ['chrome', 'firefox'], - }, - }, - name: 'html-reporter-plugin worker tests', -} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js deleted file mode 100644 index 9b5043f08..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js +++ /dev/null @@ -1,34 +0,0 @@ -const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure') - -setHeadlessWhen(process.env.HEADLESS) -setWindowSize(1600, 1200) - -exports.config = { - tests: './*_test.js', - output: './output', - helpers: { - FileSystem: {}, - }, - plugins: { - htmlReporter: { - enabled: true, - output: './output', - reportFileName: 'worker-report.html', - includeArtifacts: true, - showSteps: true, - showSkipped: true, - showMetadata: true, - showTags: true, - showRetries: true, - exportStats: false, - keepHistory: false, - }, - }, - multiple: { - parallel: { - chunks: 2, - browsers: ['chrome', 'firefox'], - }, - }, - name: 'html-reporter-plugin worker tests', -} diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.cjs b/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.cjs deleted file mode 100644 index 61e085e6c..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.cjs +++ /dev/null @@ -1,21 +0,0 @@ -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/codecept.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.js deleted file mode 100644 index 61e085e6c..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/codecept.conf.js +++ /dev/null @@ -1,21 +0,0 @@ -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/edge-cases_test.js b/test/data/sandbox/configs/html-reporter-plugin/edge-cases_test.js deleted file mode 100644 index 8c0633302..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/edge-cases_test.js +++ /dev/null @@ -1,42 +0,0 @@ -Feature('HTML Reporter Edge Cases') - -Scenario('test with special characters <>&"\'', ({ I }) => { - I.amInPath('.') - I.seeFile('package.json') -}) - -Scenario('test with very long name that should be handled properly without breaking the layout or causing any rendering issues in the HTML report', ({ I }) => { - I.amInPath('.') - I.seeFile('codecept.conf.js') -}) - -Scenario('test with unicode characters 测试 🎉 ñoño', ({ I }) => { - I.amInPath('.') - I.seeFile('package.json') -}) - -Scenario('@tag1 @tag2 @critical test with multiple tags', ({ I }) => { - I.amInPath('.') - I.seeFile('codecept.conf.js') -}) - -Scenario('test with metadata', ({ I }) => { - I.amInPath('.') - I.seeFile('package.json') -}).tag('@smoke').tag('@regression') - -Scenario('test that takes longer to execute', async ({ I }) => { - I.amInPath('.') - await new Promise(resolve => setTimeout(resolve, 500)) - I.seeFile('package.json') -}) - -Scenario('test with nested error', ({ I }) => { - I.amInPath('.') - try { - throw new Error('Nested error with tags & special chars') - } catch (e) { - // This will fail - I.seeFile('non-existent-file-with-error.txt') - } -}) 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 deleted file mode 100644 index 0af87028d..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/features/html-reporter.feature +++ /dev/null @@ -1,30 +0,0 @@ -@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 - But I should see the expected result - - @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 deleted file mode 100644 index 1ec50a97d..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/html-reporter_test.js +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index d82476379..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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/retry_test.js b/test/data/sandbox/configs/html-reporter-plugin/retry_test.js deleted file mode 100644 index 1b8631e9e..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/retry_test.js +++ /dev/null @@ -1,23 +0,0 @@ -Feature('HTML Reporter Retry Test') - -let attemptCounter = 0 - -Scenario('test that fails first time then passes', ({ I }) => { - attemptCounter++ - I.amInPath('.') - if (attemptCounter === 1) { - I.seeFile('this-file-does-not-exist.txt') // Will fail first time - } else { - I.seeFile('package.json') // Will pass on retry - } -}) - -Scenario('test that always fails even with retries', ({ I }) => { - I.amInPath('.') - I.seeFile('this-will-never-exist.txt') -}) - -Scenario('test that passes without retries', ({ I }) => { - I.amInPath('.') - I.seeFile('codecept.conf.js') -}) 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 deleted file mode 100644 index 3696fcbf7..000000000 --- a/test/data/sandbox/configs/html-reporter-plugin/step_definitions/steps.js +++ /dev/null @@ -1,46 +0,0 @@ -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/data/typescript-config-require/codecept.conf.ts b/test/data/typescript-config-require/codecept.conf.ts index 467b4964f..a6fc13a5e 100644 --- a/test/data/typescript-config-require/codecept.conf.ts +++ b/test/data/typescript-config-require/codecept.conf.ts @@ -11,10 +11,6 @@ export const config = { enabled: false, require: '@codeceptjs/allure-legacy', }, - htmlReporter: { - enabled: true, - keepHistory: true, - } }, bootstrap: null, mocha: {}, diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js deleted file mode 100644 index 8708c83ba..000000000 --- a/test/runner/html-reporter-plugin_test.js +++ /dev/null @@ -1,902 +0,0 @@ -import { expect } from 'expect' -import { exec } from 'child_process' -import { codecept_dir, codecept_run } from './consts.js' -import debug from 'debug' -import fs from 'fs' -import path from 'path' - -const log = debug('codeceptjs:tests') - -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.cjs'), (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 feature grouping with toggle - expect(reportContent).toContain('feature-group') - expect(reportContent).toContain('feature-group-title') - expect(reportContent).toContain('toggleFeatureGroup') - expect(reportContent).toContain('toggle-icon') - - // Check for test performance analysis - expect(reportContent).toContain('test-performance-section') - expect(reportContent).toContain('Test Performance Analysis') - expect(reportContent).toContain('Longest Running Tests') - expect(reportContent).toContain('Fastest Tests') - expect(reportContent).toContain('renderTestPerformance') - - // Check for enhanced history section - expect(reportContent).toContain('history-section') - expect(reportContent).toContain('Test Execution History') - expect(reportContent).toContain('historyStats') - expect(reportContent).toContain('historyTimeline') - expect(reportContent).toContain('renderHistoryTimeline') - - // 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('