import stripAnsi from 'strip-ansi'
import type { HookStats, TestStats, SuiteStats, CommandArgs, Tag } from '@wdio/reporter'
import type { Options } from '@wdio/types'
import type { AllureGroup, AllureStep, AllureTest, ExecutableItemWrapper, FixtureResult, Label, TestResult } from 'allure-js-commons'
import { LabelName, md5, Stage, Status, Status as AllureStatus } from 'allure-js-commons'
import CompoundError from './compoundError.js'
import { allHooks, eachHooks, linkPlaceholder } from './constants.js'
* Get allure test status by TestStat object
* @param test {Object} - TestStat object
* @param config {Object} - wdio config object
* @private
*/
export const getTestStatus = (
test: TestStats | HookStats,
config?: Options.Testrunner
): AllureStatus => {
if (config && config.framework === 'jasmine') {
return AllureStatus.FAILED
}
if (test.error) {
if (test.error.message) {
const message = test.error.message.trim().toLowerCase()
return message.startsWith('assertionerror') ||
message.includes('expect')
? AllureStatus.FAILED
: AllureStatus.BROKEN
}
if (test.error.stack) {
const stackTrace = test.error.stack.trim().toLowerCase()
return stackTrace.startsWith('assertionerror') ||
stackTrace.includes('expect')
? AllureStatus.FAILED
: AllureStatus.BROKEN
}
} else if (test.errors) {
return AllureStatus.FAILED
}
return AllureStatus.BROKEN
}
* Check is object is empty
* @param object {Object}
* @private
*/
export const isEmpty = (object: object) =>
!object || Object.keys(object).length === 0
* Is mocha/jasmine beforeEach hook
* @param title {String} - hook title
* @returns {boolean}
* @private
*/
export const isBeforeEachTypeHook = (title: string) =>
title.includes(eachHooks[0])
* Is mocha/jasmine beforeAll / beforeEach hook
* @param title {String} - hook title
* @returns {boolean}
* @private
*/
export const isBeforeTypeHook = (title: string) =>
title.includes(allHooks[0]) || title.includes(eachHooks[0])
* Is mocha/jasmine beforeEach / afterEach hook
* @param title {String} - hook title
* @returns {boolean}
* @private
*/
export const isEachTypeHooks = (title: string) =>
eachHooks.some((hook) => title.includes(hook))
* Is mocha/jasmine beforeAll / afterAll hook
* @param title {String} - hook title
* @returns {boolean}
* @private
*/
export const isAllTypeHooks = (title: string) =>
allHooks.some((hook) => title.includes(hook))
* Properly format error from different test runners
* @param {object} test - TestStat object
* @returns {Object} - error object
* @private
*/
export const getErrorFromFailedTest = (
test: TestStats | HookStats
): Error | CompoundError | undefined => {
if (test.errors && Array.isArray(test.errors) && test.errors.length) {
for (let i = 0; i < test.errors.length; i += 1) {
if (test.errors[i].message) {
test.errors[i].message = stripAnsi(test.errors[i].message)
}
if (test.errors[i].stack) {
test.errors[i].stack = stripAnsi(test.errors[i].stack!)
}
}
return test.errors.length === 1
? test.errors[0]
: new CompoundError(...(test.errors as Error[]))
}
if (test.error) {
if (test.error.message) {
test.error.message = stripAnsi(test.error.message)
}
if (test.error.stack) {
test.error.stack = stripAnsi(test.error.stack)
}
}
return test.error
}
* Update the hook information with the new one, it could be use when a hook ends
* @param {HookStats} newHookStats - New information that will be applied to current hook info
* @param {ExecutableItemWrapper} hookElement - hook element registered in the report
* @param {AllureStep} hookRootStep - root hook step
*
* @private
*/
export const getHookStatus = (newHookStats: HookStats, hookElement: ExecutableItemWrapper, hookRootStep: AllureStep) => {
hookElement.stage = hookRootStep.stage =
Stage.FINISHED
const formattedError = getErrorFromFailedTest(newHookStats)
hookElement.detailsMessage = hookRootStep.detailsMessage = formattedError?.message
hookElement.detailsTrace = hookRootStep.detailsTrace = formattedError?.stack
let finalStatus = Status.PASSED
const hookSteps = hookRootStep.wrappedItem.steps
if (Array.isArray(hookSteps) && hookSteps.length) {
const statusPriority = {
[Status.FAILED]: 0,
[Status.BROKEN]: 1,
[Status.SKIPPED]: 2,
[Status.PASSED]: 3,
}
let stepStatus = Status.PASSED
for (const step of hookSteps) {
if (step.status && statusPriority[step.status] < statusPriority[finalStatus]) {
stepStatus = step.status
}
}
finalStatus = stepStatus === Status.FAILED? Status.BROKEN : stepStatus
} else if (newHookStats.error || (Array.isArray(newHookStats.errors) && newHookStats.errors.length)){
finalStatus = Status.BROKEN
}
hookElement.status = hookRootStep.status = finalStatus
}
export const cleanCucumberHooks = (hook: FixtureResult | TestResult) => {
const currentStep = hook.steps[hook.steps.length - 1]
if (
currentStep &&
currentStep.steps.length === 0 &&
currentStep.attachments.length === 0 &&
hook.attachments.length === 0 &&
currentStep.status === Status.PASSED
) {
hook.steps.pop()
}
}
* Substitute task id to link template
* @param {string} template - link template
* @param {string} id - task id
* @returns {string} - link after substitution
* @private
*/
export const getLinkByTemplate = (template: string | undefined, id: string) => {
if (typeof template !== 'string') {
return id
}
if (!template.includes(linkPlaceholder)) {
throw Error(
`The link template "${template}" must contain ${linkPlaceholder} substring.`
)
}
return template.replace(linkPlaceholder, id)
}
export const findLast = <T>(
arr: Array<T>,
predicate: (el: T) => boolean
): T | undefined => {
let result: T | undefined
for (let i = arr.length - 1; i >= 0; i--) {
if (predicate(arr[i])) {
result = arr[i]
break
}
}
return result
}
export const isScreenshotCommand = (command: CommandArgs): boolean => {
const isScrenshotEndpoint = /\/session\/[^/]*(\/element\/[^/]*)?\/screenshot/
return (
(command.endpoint && isScrenshotEndpoint.test(command.endpoint)) ||
command.command === 'takeScreenshot'
)
}
export const getSuiteLabels = ({ tags }: SuiteStats): Label[] => {
if (!tags) {
return []
}
return (tags as Tag[]).reduce<Label[]>((acc, tag: Tag) => {
const label = tag.name.replace(/[@]/, '').split('=')
if (label.length === 2) {
return acc.concat({ name: label[0], value: label[1] })
}
return acc
}, [])
}
export const setAllureIds = (test: AllureTest | undefined, suite: AllureGroup | undefined) => {
if (!test) {
return
}
const params = test.wrappedItem.parameters.slice()
const paramsPart = params
.sort((a, b) => a.name?.localeCompare(b.name))
.map(it => it.name + it.value)
.join('')
const hash = md5(`${suite?.name}${test.wrappedItem.name}${paramsPart}`)
test.historyId = hash
if ('labels' in test.wrappedItem) {
if (test.wrappedItem.labels?.find((label: Label) => label.name === LabelName.AS_ID)) {
return
}
}
test.testCaseId = hash
}