wdio-mochawesome-reporter
Version:
A WebdriverIO plugin. Generates json results in Mochawesome format.
325 lines (280 loc) • 11 kB
JavaScript
const WDIOReporter = require('@wdio/reporter').default
const Suite = require('./suite')
const Stats = require('./stats')
const Test = require('./test')
const fs = require('fs')
const path = require('path')
class WdioMochawesomeReporter extends WDIOReporter {
constructor (options) {
options = Object.assign(options)
super(options)
this.pendingContext = []
this.registerListeners()
}
onRunnerStart (runner) {
this.config = runner.config
this.sanitizedCaps = runner.sanitizedCapabilities
// Set sessionId
if (runner.isMultiremote) {
this.sessionId = {}
for (const name in runner.capabilities) {
this.sessionId[name] = runner.capabilities[name].sessionId
}
} else {
this.sessionId = runner.sessionId
}
// mochawesome requires this root suite for HTML report generation to work properly
this.results = {
stats: new Stats(runner.start),
results: [new Suite(true, { title: '' })],
meta: {
mocha: {
version: '11.1.0'
},
mochawesome: {
options: {
quiet: false,
reportFilename: 'test-report',
saveHtml: true,
saveJson: true,
consoleReporter: 'spec',
useInlineDiffs: false,
code: false
},
version: '7.1.3'
},
marge: {
options: {
reportDir: 'mochawesome-report',
reportFilename: 'test-report',
reportTitle: 'WebdriverIO Tests',
charts: 'true',
overwrite: 'true',
timestamp: 'true',
code: 'false',
autoOpen: 'true'
},
version: '6.2.0'
}
}
}
}
onSuiteStart (suite) {
this.currSuite = new Suite(false, suite, this.sanitizedCaps)
this.results.stats.incrementSuites()
// Extract tests from spec file if available
if (suite.file) {
try {
const tests = this.extractTestsFromSpecFile(suite.file)
if (tests.length > 0) {
// Add the extracted tests to a global cache for external access
if (!WdioMochawesomeReporter.testCache) {
WdioMochawesomeReporter.testCache = {}
}
// Use normalized path as key
const normalizedPath = path.resolve(suite.file)
WdioMochawesomeReporter.testCache[normalizedPath] = tests
}
} catch (error) {
console.error(`Error extracting tests from ${suite.file}:`, error.message)
}
}
}
// Extract tests from spec file
extractTestsFromSpecFile (filePath) {
try {
// Make sure file exists
if (!fs.existsSync(filePath)) {
console.error(`Spec file not found: ${filePath}`)
return []
}
// Read file content
const content = fs.readFileSync(filePath, 'utf-8')
// Use regex to find it blocks with different patterns
// Supports:
// - it('test name', function() {...})
// - it("test name", () => {...})
// - it(`test name`, async () => {...})
// - test('test name', function() {...})
const itBlockRegex = /(?:it|test)\s*\(\s*(['"`])(.+?)\1\s*,\s*(?:async\s*)?\(?(?:function\s*)?\(?[\w,\s]*\)?\s*(?:=>)?\s*{/g
const tests = []
let match
// Extract all matches
while ((match = itBlockRegex.exec(content)) !== null) {
// Find surrounding describe blocks to add more context
const surroundingCode = content.substring(0, match.index)
const describeBlocks = this.getDescribeContext(surroundingCode)
tests.push({
title: match[2],
line: this.getLineNumber(content, match.index),
context: '',
state: 'none',
fullTitle: [...describeBlocks, match[2]].join(' > ')
})
}
return tests
} catch (error) {
console.error(`Error reading spec file ${filePath}:`, error.message)
return []
}
}
// Extract describe blocks surrounding a test for context
getDescribeContext (codeUpToTest) {
const describeRegex = /describe\s*\(\s*(['"`])(.+?)\1\s*,/g
const describes = []
let match
while ((match = describeRegex.exec(codeUpToTest)) !== null) {
describes.push(match[2])
}
return describes
}
// Helper to get line number from character index
getLineNumber (content, index) {
const lines = content.substring(0, index).split('\n')
return lines.length
}
onTestStart (test) {
this.currTest = new Test(test, this.currSuite.uuid)
this.currTest.addSessionContext(this.sessionId)
// Apply any pending context that was added before the test started
if (this.pendingContext && this.pendingContext.length > 0) {
this.pendingContext.forEach(context => {
this.currTest.context.push(context)
})
this.pendingContext = []
}
}
onTestSkip (test) {
this.currTest = new Test(test, this.currSuite.uuid)
this.currTest.addSessionContext(this.sessionId)
// Apply any pending context that was added before the test started
if (this.pendingContext && this.pendingContext.length > 0) {
this.pendingContext.forEach(context => {
this.currTest.context.push(context)
})
this.pendingContext = []
}
}
onHookStart (hook) {
// Create a test object for the hook
this.currHook = new Test({
title: hook.title,
fullTitle: hook.title,
type: 'hook'
}, this.currSuite ? this.currSuite.uuid : null)
// Add session context to hook
if (this.sessionId) {
this.currHook.addSessionContext(this.sessionId)
}
// Apply any pending context
if (this.pendingContext && this.pendingContext.length > 0) {
this.pendingContext.forEach(context => {
this.currHook.context.push(context)
})
this.pendingContext = []
}
}
onHookEnd (hook) {
if (!this.currHook) return
this.currHook.duration = hook._duration
// Handle results
if (hook.error) {
this.currHook.state = 'failed'
this.currHook.fail = true
this.currHook.err = {
message: hook.error.message,
estack: hook.error.stack,
diff: null
}
for (const test of WdioMochawesomeReporter.testCache[this.currSuite.file]) {
if (test.state === 'none') {
const skippedTest = new Test(test, this.currSuite.uuid)
test.state = 'skipped'
skippedTest.updateResult(test)
// Stringify context only if it exists
if (skippedTest.context) {
skippedTest.context = JSON.stringify(skippedTest.context)
}
this.currSuite.addTest(skippedTest)
this.results.stats.incrementTests(skippedTest)
}
}
} else {
this.currHook.state = 'passed'
this.currHook.pass = true
}
// Stringify context only if it exists
if (this.currHook.context) {
this.currHook.context = JSON.stringify(this.currHook.context)
}
// Add to appropriate hook collection
if (this.currSuite) {
if (hook.title.includes('before')) {
this.currSuite.addBeforeHook(this.currHook)
} else {
this.currSuite.addAfterHook(this.currHook)
}
}
this.currHook = null
}
onAfterCommand (cmd) {
const isScreenshotEndpoint = /\/session\/[^/]*\/screenshot/
if (isScreenshotEndpoint.test(cmd.endpoint) && cmd.result.value) {
this.currTest.addScreenshotContext(cmd.result.value)
}
}
onTestEnd (test) {
this.currTest.duration = test._duration
this.currTest.updateResult(test)
// Initialize testCache for the current file if not already done
if (!WdioMochawesomeReporter.testCache || !WdioMochawesomeReporter.testCache[this.currSuite.file]) {
if (!WdioMochawesomeReporter.testCache) {
WdioMochawesomeReporter.testCache = {}
}
WdioMochawesomeReporter.testCache[this.currSuite.file] = []
}
for (const cachedTest of WdioMochawesomeReporter.testCache[this.currSuite.file]) {
if (cachedTest.title === test.title) {
cachedTest.state = test.state.toLowerCase()
}
}
// Only stringify context if it exists
if (this.currTest.context) {
this.currTest.context = JSON.stringify(this.currTest.context)
}
this.currSuite.addTest(this.currTest)
this.results.stats.incrementTests(this.currTest)
}
onSuiteEnd (suite) {
this.currSuite.duration = suite.duration
this.results.results[0].addSuite(this.currSuite)
}
onRunnerEnd (runner) {
this.results.stats.end = runner.end
this.results.stats.duration = runner.duration
this.write(JSON.stringify(this.results))
}
// addContext functionality
registerListeners () {
process.on('wdio-mochawesome-reporter:addContext', this.addSomeContext.bind(this))
}
addSomeContext (object) {
if (this.currTest && this.currTest.context) {
this.currTest.context.push(object)
} else {
// Store context for tests that haven't started yet
if (!this.pendingContext) {
this.pendingContext = []
}
this.pendingContext.push(object)
}
}
static addContext (context) {
process.emit('wdio-mochawesome-reporter:addContext', context)
}
// Clear the test cache (useful for testing or when files change)
static clearTestCache () {
WdioMochawesomeReporter.testCache = {}
}
}
exports.default = WdioMochawesomeReporter