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) => { // stage to finish for all hook. hookElement.stage = hookRootStep.stage = Stage.FINISHED // set error detail information const formattedError = getErrorFromFailedTest(newHookStats) hookElement.detailsMessage = hookRootStep.detailsMessage = formattedError?.message hookElement.detailsTrace = hookRootStep.detailsTrace = formattedError?.stack let finalStatus = Status.PASSED // set status to hook root step 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 = ( arr: Array, 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 ( // WebDriver protocol (command.endpoint && isScrenshotEndpoint.test(command.endpoint)) || // DevTools protocol command.command === 'takeScreenshot' ) } export const getSuiteLabels = ({ tags }: SuiteStats): Label[] => { if (!tags) { return [] } return (tags as Tag[]).reduce((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 }