import os from 'node:os'
import path from 'node:path'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { TestStats } from '@wdio/reporter'
import WDIOJunitReporter from '../src/index.js'
import type { SuiteStats } from '@wdio/reporter'
const mochaRunnerLog = (await vi.importActual('./__fixtures__/mocha-runner.json') as any).default
const mochaRunnerNestedArrayOfSuitesLog = (await vi.importActual('./__fixtures__/mocha-runner-nested-array-specs.json') as any).default
const cucumberRunnerLog = (await vi.importActual('./__fixtures__/cucumber-runner.json') as any).default
const cucumberRunnerBrowserstackIosLog = (await vi.importActual('./__fixtures__/cucumber-runner-browserstack-ios.json') as any).default
const cucumberRunnerBrowserstackAndroidLog = (await vi.importActual('./__fixtures__/cucumber-runner-browserstack-android.json') as any).default
const cucumberRunnerBrowserstackAndroidLogMissingOS = (await vi.importActual('./__fixtures__/cucumber-runner-browserstack-android-missing-os.json') as any).default
const suitesLog = (await vi.importActual('./__fixtures__/suites.json') as any).default
const suitesWithNoErrorObjectLog = (await vi.importActual('./__fixtures__/suites-with-no-error-object.json') as any).default
const featuresLog = (await vi.importActual('./__fixtures__/cucumber-features.json') as any).default
const featuresWithFailingThenSkipStepLog = (await vi.importActual('./__fixtures__/cucumber-features-with-failed-then-skipped-steps.json') as any).default
const featuresWithPendingStepLog = (await vi.importActual('./__fixtures__/cucumber-features-with-pending-step.json') as any).default
const featuresWithErrorStepAndNoErrorObjectLog = (await vi.importActual('./__fixtures__/cucumber-features-with-error-step-and-no-error-object.json') as any).default
const nestedSuites = (await vi.importActual('./__fixtures__/nested-suites.json') as any).default
const nestedArrayOfSuites = (await vi.importActual('./__fixtures__/nested-array-suites.json') as any).default
const unorderedFeatureAndScenarioWithError = (await vi.importActual('./__fixtures__/cucumber-features-with-error-step-and-no-error-object-unordered.json') as any).default
const suitesWithFailedBeforeEachHookLog = (await vi.importActual('./__fixtures__/suites-with-failed-before-each-hook.json') as any).default
const suitesWithFailedAfterEachHookLog = (await vi.importActual('./__fixtures__/suites-with-failed-after-each-hook.json') as any).default
const suitesHooksLog = (await vi.importActual('./__fixtures__/suites-hooks.json') as any).default
const suiteTestRetry = (await vi.importActual('./__fixtures__/suite-test-retry.json') as any).default
const suitesMultipleLog = (await vi.importActual('./__fixtures__/suites-multiple.json') as any).default
const suitesErrorLog = (await vi.importActual('./__fixtures__/suites-error.json') as any).default
const suiteEmpty = (await vi.importActual('./__fixtures__/suite-empty.json') as any).default
vi.mock('@wdio/reporter', () => import(path.join(process.cwd(), '__mocks__', '@wdio/reporter')))
if (os.platform() === 'win32') {
cucumberRunnerLog.specs = ['file:///C:/features/sample_feature.feature']
mochaRunnerLog.specs = ['file:///C:/path/to/project/test/specs/sync.spec.js']
mochaRunnerNestedArrayOfSuitesLog.specs = ['file:///C:/path/to/project/test/specs/sync_0.spec.js', 'file:///C:/path/to/project/test/specs/sync_1.spec.js']
cucumberRunnerBrowserstackAndroidLogMissingOS.specs = ['file:///C:/features/sample_feature.feature']
cucumberRunnerBrowserstackAndroidLog.specs = ['file:///C:/features/sample_feature.feature']
cucumberRunnerBrowserstackIosLog.specs = ['file:///C:/features/sample_feature.feature']
for (const fixture of [featuresLog, featuresWithPendingStepLog, unorderedFeatureAndScenarioWithError, featuresWithErrorStepAndNoErrorObjectLog, featuresWithFailingThenSkipStepLog]) {
for (const [, suite] of Object.entries(fixture) as any) {
suite.file = 'C:\\features\\sample_feature.feature'
}
}
for (const fixture of [suitesLog, suitesErrorLog, suitesHooksLog, suiteTestRetry, suitesMultipleLog, suitesWithFailedAfterEachHookLog, suitesWithFailedBeforeEachHookLog, suitesWithNoErrorObjectLog, nestedArrayOfSuites, nestedSuites, suiteEmpty]) {
for (const [index, [, suite]] of Object.entries(Object.entries(fixture)) as any) {
const specFileName = (fixture === nestedArrayOfSuites) ? `sync_${index}` : 'sync'
suite.file = `C:\\path\\to\\project\\test\\specs\\${specFileName}.spec.js`
}
}
}
describe('wdio-junit-reporter', () => {
let reporter: WDIOJunitReporter
beforeEach(() => {
reporter = new WDIOJunitReporter({ stdout: true })
})
it('should write to output stream on runnerEnd', () => {
reporter['_buildJunitXml'] = vi.fn().mockReturnValue(undefined)
reporter.write = vi.fn()
reporter.onRunnerEnd({} as any)
expect(vi.mocked(reporter.write).mock.calls[0][0]).toMatchSnapshot()
})
it('should prepare name', () => {
expect(reporter['_prepareName']()).toMatchSnapshot()
expect(reporter['_prepareName']('Chrome something_odd.foobar')).toMatchSnapshot()
expect(reporter['_prepareName']('Chrome @something_odd.foobar')).toMatchSnapshot()
})
it('has a suiteNameFormat option for setting names manually', () => {
reporter = new WDIOJunitReporter({ stdout: true, suiteNameFormat: /foobar/ })
expect(reporter['_prepareName']('Skipped_foobar_test')).toMatchSnapshot()
})
it('can format output', () => {
expect(reporter['_format'](1)).toMatchSnapshot()
expect(reporter['_format']({ a: 'foo', b: 'bar' })).toMatchSnapshot()
expect(reporter['_format']({ a: 'foobar'.repeat(100) })).toMatchSnapshot()
expect(reporter['_format']({ a: '@'.repeat(10000) })).toMatchSnapshot()
expect(reporter['_format']({ a: '@'.repeat(100).split('') })).toMatchSnapshot()
expect(reporter['_format']([...Array(11)].map((item, i) => i).reduce((a, b) => {
a['entry' + b] = 'foobar'
return a
}, {} as Record<string, string>))).toBe('{"entry0":"foobar","entry1":"foobar","entry2":"foobar","entry3":"foobar","entry4":"foobar","entry5":"foobar","entry6":"foobar","entry7":"foobar","entry8":"foobar","entry9":"foobar","_":"1 more keys: [\\"entry10\\"]"}')
})
it('can translate stdout', () => {
const teststats: TestStats = {
output: [{
cid: '123',
type: 'command',
method: 'POST',
sessionId: 'foobar',
endpoint: '/sessionId/click',
body: { elementId: 'foobar' },
command: 'getText',
params: {},
result: { value: 'some text' }
}, {
cid: '123',
method: 'POST',
sessionId: 'foobar',
endpoint: '/sessionId/click',
command: 'getText',
params: {},
result: { value: 'some text' },
type: 'result',
body: { value: 'foobar' }
}]
} as unknown as TestStats
expect(reporter['_getStandardOutput'](teststats)).toContain('COMMAND: POST /sessionId/click - {"elementId":"foobar"}')
expect(reporter['_getStandardOutput'](teststats)).toContain('RESULT: {"value":"foobar"}')
})
it('test is marked as skipped when is retried', () => {
const testStats = new TestStats({
type: 'test:start',
uid: 'test-00-0',
cid: '0-0',
title: 'test',
fullTitle: 'suite test',
retries: 0,
parent: 'suite',
pending: false,
specs: []
})
reporter['onTestRetry'](testStats)
expect(testStats.state).toContain('skipped')
})
it('generates xml output', () => {
reporter.suites = suitesLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output (Cucumber-style)', () => {
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output (Cucumber-style) (with packageName)', () => {
reporter = new WDIOJunitReporter({ packageName: 'wdio-unit-tests', stdout: true })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('scenario will be marked failed if a single scenario step fails (Cucumber-style)', () => {
reporter.suites = featuresWithFailingThenSkipStepLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('scenario will be marked failed if a single scenario step throws an error but there is no error object in JSON', () => {
reporter.suites = suitesWithNoErrorObjectLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('scenario will be marked failed if a single scenario step throws an error but there is no error object in JSON (Cucumber-style)', () => {
reporter.suites = featuresWithErrorStepAndNoErrorObjectLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('should build the xml if feature and scenario are unordered in JSON (Cucumber-style)', () => {
reporter.suites = unorderedFeatureAndScenarioWithError as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('scenario will be marked skipped if a single scenario step is pending (Cucumber-style)', () => {
reporter.suites = featuresWithPendingStepLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output if before all hook failed', () => {
reporter.suites = suitesHooksLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output if before each hook failed', () => {
reporter.suites = suitesWithFailedBeforeEachHookLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output if after each hook failed', () => {
reporter.suites = suitesWithFailedAfterEachHookLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output for multiple describe blocks', () => {
reporter.suites = suitesMultipleLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output when test is marked as skipped', () => {
reporter.suites = suiteTestRetry as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when error options are set', () => {
const errorOptions = {
error: 'message',
failure: 'message',
stacktrace: 'stack'
}
reporter = new WDIOJunitReporter({ stdout: true, errorOptions })
reporter.suites = suitesLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when error options are set (Cucumber-style)', () => {
const errorOptions = {
error: 'message',
failure: 'message',
stacktrace: 'stack'
}
reporter = new WDIOJunitReporter({ stdout: true, errorOptions })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output with correct information when test is ran against Browserstack for iOS apps', () => {
reporter = new WDIOJunitReporter({ stdout: true })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerBrowserstackIosLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output with correct information when test is ran against Browserstack for Android apps', () => {
reporter = new WDIOJunitReporter({ stdout: true })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerBrowserstackAndroidLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('ensures that capabilities passed to buildJunitXml are not null/undefined', () => {
reporter = new WDIOJunitReporter({})
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerBrowserstackAndroidLogMissingOS as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when the addFileAttribute option is set', () => {
reporter = new WDIOJunitReporter({ stdout: true, addFileAttribute: true })
reporter.suites = suitesLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when the addFileAttribute option is set (Cucumber-style)', () => {
reporter = new WDIOJunitReporter({ stdout: true, addFileAttribute: true })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output without ansi', () => {
reporter.suites = suitesErrorLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when having nested suites', () => {
reporter.suites = nestedSuites as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when having nested array of suites', () => {
reporter.suites = nestedArrayOfSuites as any
expect(reporter['_buildJunitXml'](mochaRunnerNestedArrayOfSuitesLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when having classNameFormat override with mocha', () => {
reporter = new WDIOJunitReporter({ stdout: true, classNameFormat: ({ packageName, suite }) => `foo-${packageName}-${suite!.fullTitle}` })
reporter.suites = suitesErrorLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('generates xml output correctly when having classNameFormat override with cucumber', () => {
reporter = new WDIOJunitReporter({ stdout: true, classNameFormat: ({ packageName, activeFeatureName }) => `foo-${packageName}-${activeFeatureName}` })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace('C:/', '')).toMatchSnapshot()
})
it('generates xml output correctly when having testSuiteNameFormat override with mocha', () => {
reporter = new WDIOJunitReporter({ stdout: true, suiteNameFormat: ({ name, suite }) => `foo ${name} ${suite.title}` })
reporter.suites = suitesErrorLog as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace('file://', '').replace('C:/', '')).toMatchSnapshot()
})
it('generates xml output correctly when having testSuiteNameFormat override with cucumber', () => {
reporter = new WDIOJunitReporter({ stdout: true, suiteNameFormat: ({ name, suite }) => `foo-${name}-${suite.title}` })
reporter.suites = featuresLog as any
expect(reporter['_buildJunitXml'](cucumberRunnerLog as any).replace(/\s/g, '').replace('C:/', '')).toMatchSnapshot()
})
it('generates xml output correctly with empty suite', () => {
reporter.suites = suiteEmpty as any
expect(reporter['_buildJunitXml'](mochaRunnerLog as any).replace(/\s/g, '').replace(/file:\/\//g, '').replace(/C:\//g, '')).toMatchSnapshot()
})
it('_buildOrderedReport', () => {
reporter = new WDIOJunitReporter({ stdout: true, suiteNameFormat: ({ name, suite }) => `foo-${name}-${suite.title}` })
reporter['_addCucumberFeatureToBuilder'] = () => '_addCucumberFeatureToBuilder'
reporter['_addSuiteToBuilder'] = () => '_addSuiteToBuilder'
const specFileName = os.platform() === 'win32' ? 'file:///C:/foo/bar' : 'file:///foo/bar'
const file = os.platform() === 'win32' ? 'C:\\foo\\bar' : '/foo/bar'
reporter.suites = {
foo: { type: 'feature' } as any,
bar: { type: 'feature', file } as any
}
expect(reporter['_buildOrderedReport'](null, null as any, specFileName, 'feature', true)).toBe('_addCucumberFeatureToBuilder')
})
it('_sameFileName - case independent on win32', function (context) {
if (os.platform() !== 'win32') {
context.skip()
}
expect(reporter['_sameFileName']('file:///C:/foo/bar', 'file:///C:/foo/bar')).toBeTruthy()
expect(reporter['_sameFileName']('file:///c:/foo/bar', 'file:///C:/foo/bar')).toBeTruthy()
expect(reporter['_sameFileName']('file:///c:/foo/bar', 'C:\\foo\\bar')).toBeTruthy()
expect(reporter['_sameFileName']('file:///C:/foo/bar', 'c:\\foo\\bar')).toBeTruthy()
expect(reporter['_sameFileName']('file:///C:/foo/bar', 'C:\\FOO\\bar')).toBeTruthy()
expect(reporter['_sameFileName']('file:///c:/foo/bar', 'C:\\bar\\foo')).toBeFalsy()
})
it('_sameFileName - case sensitive on everything except win32', function (context) {
if (os.platform() === 'win32') {
context.skip()
}
expect(reporter['_sameFileName']('file:///foo/bar', 'file:///Foo/bar')).toBeFalsy()
expect(reporter['_sameFileName']('file:///foo/bar', '/Foo/bar')).toBeFalsy()
})
it('_sameFileName', () => {
reporter = new WDIOJunitReporter({ stdout: true })
const file1URL = os.platform() === 'win32' ? 'file:///C:/foo/bar' : 'file:///foo/bar'
const file1Path = os.platform() === 'win32' ? 'C:\\foo\\bar' : '/foo/bar'
const file2URL = os.platform() === 'win32' ? 'file:///C:/bar/foo' : 'file:///bar/foo'
const file2Path = os.platform() === 'win32' ? 'C:\\bar\\foo' : '/bar/foo'
expect(reporter['_sameFileName'](file1URL, file1URL)).toBeTruthy()
expect(reporter['_sameFileName'](file1URL, file1Path)).toBeTruthy()
expect(reporter['_sameFileName'](file1Path, file1URL)).toBeTruthy()
expect(reporter['_sameFileName'](file1Path, file1Path)).toBeTruthy()
expect(reporter['_sameFileName'](file1URL, file2URL)).toBeFalsy()
expect(reporter['_sameFileName'](file1URL, file2Path)).toBeFalsy()
expect(reporter['_sameFileName'](file1Path, file2URL)).toBeFalsy()
expect(reporter['_sameFileName'](file1Path, file2Path)).toBeFalsy()
expect(reporter['_sameFileName'](undefined, file1URL)).toBeFalsy()
expect(reporter['_sameFileName'](undefined, file1Path)).toBeFalsy()
expect(reporter['_sameFileName'](file1URL, undefined)).toBeFalsy()
expect(reporter['_sameFileName'](file1Path, undefined)).toBeFalsy()
expect(reporter['_sameFileName'](undefined, undefined)).toBeTruthy()
})
const options = { stdout: true, addWorkerLogs: true }
it('addWorkerLogs: should add worker console log to report for test if activated', () => {
reporter = new WDIOJunitReporter(options)
const suite = Object.values(suitesLog)[0] as SuiteStats
reporter.onSuiteStart(suite)
const test1 = suite.tests[0]
const test2 = suite.tests[1]
reporter.onTestStart(test1)
reporter['_appendConsoleLog']('0 - line 0', 'utf-8', () => {})
reporter['_appendConsoleLog']('0 - line 1', 'utf-8', () => {})
reporter.onTestPass(test1)
reporter.onTestStart(test2)
reporter['_appendConsoleLog']('1 - line 0', 'utf-8', () => {})
reporter['_appendConsoleLog']('1 - line 1', 'utf-8', () => {})
reporter.onTestPass(suite.tests[0])
expect(reporter['_getStandardOutput'](test1).toString()).toContain('0 - line 1')
expect(reporter['_getStandardOutput'](test2).toString()).toContain('1 - line 1')
expect(reporter['_getStandardOutput'](test1).toString()).not.toContain('1 - line 1')
expect(reporter['_getStandardOutput'](test2).toString()).not.toContain('0 - line 1')
})
it('addProperty adds a property to currently running testcase', () => {
reporter = new WDIOJunitReporter(options)
reporter.suites = suitesLog as any
const suite = Object.values(suitesLog)[0] as SuiteStats
reporter.onSuiteStart(suite)
const test1 = suite.tests[0]
const test2 = suite.tests[1]
reporter.onTestStart(test1)
reporter['_addPropertyToCurrentTest']({ name: '0-prop1', value: '0-value' })
reporter.onTestPass(test1)
reporter.onTestStart(test2)
reporter['_addPropertyToCurrentTest']({ name: '1-prop1', value: '1-value' })
reporter.onTestPass(suite.tests[0])
const output = reporter['_buildJunitXml'](mochaRunnerLog).toString()
expect(output).toContain('<property name="0-prop1" value="0-value"/>')
})
})