import path from 'node:path' import type { Frameworks } from '@wdio/types' import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter' import { v4 as uuidv4 } from 'uuid' import type { CucumberStore, Feature, Scenario, Step, FeatureChild, CucumberHook, CucumberHookParams, Pickle, ITestCaseHookParameter } from './cucumber-types.js' import TestReporter from './reporter.js' import type { BrowserstackConfig, BrowserstackOptions } from './types.js' import { frameworkSupportsHook, getCloudProvider, getFailureObject, getGitMetaData, getHookType, getPlatformVersion, getScenarioExamples, getUniqueIdentifier, getUniqueIdentifierForCucumber, isBrowserstackSession, isScreenshotCommand, isUndefined, o11yClassErrorHandler, removeAnsiColors, getObservabilityProduct, generateHashCodeFromFields } from './util.js' import type { TestData, TestMeta, PlatformMeta, CurrentRunInfo, StdLog, CBTData, IntegrationObject } from './types.js' import { BStackLogger } from './bstackLogger.js' import type { Capabilities } from '@wdio/types' import Listener from './testOps/listener.js' import { TESTOPS_SCREENSHOT_ENV } from './constants.js' class _InsightsHandler { private _tests: Record = {} private _hooks: Record = {} private _platformMeta: PlatformMeta private _commands: Record = {} private _gitConfigPath?: string private _suiteFile?: string public static currentTest: CurrentRunInfo = {} private _currentHook: CurrentRunInfo = {} private _cucumberData: CucumberStore = { stepsStarted: false, scenariosStarted: false, steps: [] } private _userCaps?: Capabilities.ResolvedTestrunnerCapabilities = {} private _options?: BrowserstackConfig & BrowserstackOptions private listener = Listener.getInstance() public currentTestId: string | undefined public cbtQueue: Array = [] constructor (private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, private _framework?: string, _userCaps?: Capabilities.ResolvedTestrunnerCapabilities, _options?: BrowserstackConfig & BrowserstackOptions) { const caps = (this._browser as WebdriverIO.Browser).capabilities as WebdriverIO.Capabilities const sessionId = (this._browser as WebdriverIO.Browser).sessionId this._userCaps = _userCaps this._options = _options this._platformMeta = { browserName: caps.browserName, browserVersion: caps?.browserVersion, platformName: caps?.platformName, caps: caps, sessionId, product: getObservabilityProduct(_options, this._isAppAutomate()) } this.registerListeners() } _isAppAutomate(): boolean { const browserDesiredCapabilities = (this._browser?.capabilities ?? {}) const desiredCapabilities = (this._userCaps ?? {}) as WebdriverIO.Capabilities return !!browserDesiredCapabilities['appium:app'] || !!desiredCapabilities['appium:app'] || !!(desiredCapabilities['appium:options']?.app) } registerListeners() { if (!(this._framework === 'mocha' || this._framework === 'cucumber')) { return } process.removeAllListeners(`bs:addLog:${process.pid}`) process.on(`bs:addLog:${process.pid}`, this.appendTestItemLog.bind(this)) } setSuiteFile(filename: string) { this._suiteFile = filename } async before () { if (isBrowserstackSession(this._browser)) { await (this._browser as WebdriverIO.Browser).executeScript(`browserstack_executor: ${JSON.stringify({ action: 'annotate', arguments: { data: `ObservabilitySync:${Date.now()}`, level: 'debug' } })}`, []) } const gitMeta = await getGitMetaData() if (gitMeta) { this._gitConfigPath = gitMeta.root } } getCucumberHookType(test: CucumberHook|undefined) { let hookType = null if (!test) { hookType = this._cucumberData.scenariosStarted ? 'AFTER_ALL' : 'BEFORE_ALL' } else if (!this._cucumberData.stepsStarted) { hookType = 'BEFORE_EACH' } else if (this._cucumberData.steps?.length > 0) { // beforeStep or afterStep } else { hookType = 'AFTER_EACH' } return hookType } getCucumberHookName(hookType: string|undefined): string { switch (hookType) { case 'BEFORE_EACH': case 'AFTER_EACH': return `${hookType} for ${this._cucumberData.scenario?.name}` case 'BEFORE_ALL': case 'AFTER_ALL': return `${hookType} for ${this._cucumberData.feature?.name}` } return '' } getCucumberHookUniqueId(hookType: string, hook: CucumberHook|undefined): string|null { switch (hookType) { case 'BEFORE_EACH': case 'AFTER_EACH': return (hook as CucumberHook).hookId case 'BEFORE_ALL': case 'AFTER_ALL': // Can only work for single beforeAll or afterAll return `${hookType} for ${this.getCucumberFeatureUniqueId()}` } return null } getCucumberFeatureUniqueId() { const { uri, feature } = this._cucumberData return `${uri}:${feature?.name}` } setCurrentHook(hookDetails: CurrentRunInfo) { if (hookDetails.finished) { if (this._currentHook.uuid === hookDetails.uuid) { this._currentHook.finished = true } return } this._currentHook = { uuid: hookDetails.uuid, finished: false } } async sendScenarioObjectSkipped(scenario: Scenario, feature: Feature, uri: string) { const testMetaData: TestMeta = { uuid: uuidv4(), startedAt: (new Date()).toISOString(), finishedAt: (new Date()).toISOString(), scenario: { name: scenario.name }, feature: { path: uri, name: feature.name, description: feature.description }, steps: scenario.steps.map((step: Step) => { return { id: step.id, text: step.text, keyword: step.keyword, result: 'skipped', } }), } this.listener.testFinished(this.getTestRunDataForCucumber(null, 'TestRunSkipped', testMetaData)) } async processCucumberHook(test: CucumberHook|undefined, params: CucumberHookParams, result?: Frameworks.TestResult) { const hookType = this.getCucumberHookType(test) if (!hookType) { return } const { event, hookUUID } = params const hookId = this.getCucumberHookUniqueId(hookType, test) if (!hookId) { return } if (event === 'before') { this.setCurrentHook({ uuid: hookUUID }) const hookMetaData = { uuid: hookUUID, startedAt: (new Date()).toISOString(), testRunId: InsightsHandler.currentTest.uuid, hookType: hookType } this._tests[hookId] = hookMetaData this.listener.hookStarted(this.getHookRunDataForCucumber(hookMetaData, 'HookRunStarted')) } else { this._tests[hookId].finishedAt = (new Date()).toISOString() this.setCurrentHook({ uuid: this._tests[hookId].uuid, finished: true }) this.listener.hookFinished(this.getHookRunDataForCucumber(this._tests[hookId], 'HookRunFinished', result)) if (hookType === 'BEFORE_ALL' && result && !result.passed) { const { feature, uri } = this._cucumberData if (!feature) { return } feature.children.map(async (childObj: FeatureChild) => { if (childObj.rule) { childObj.rule.children.map(async (scenarioObj: FeatureChild) => { if (scenarioObj.scenario) { await this.sendScenarioObjectSkipped(scenarioObj.scenario, feature, uri as string) } }) } else if (childObj.scenario) { await this.sendScenarioObjectSkipped(childObj.scenario, feature, uri as string) } }) } } } async beforeHook (test: Frameworks.Test|CucumberHook|undefined, context: unknown) { if (!frameworkSupportsHook('before', this._framework)) { return } const hookUUID = uuidv4() if (this._framework === 'cucumber') { test = test as CucumberHook|undefined await this.processCucumberHook(test, { event: 'before', hookUUID }) return } test = test as Frameworks.Test const fullTitle = getUniqueIdentifier(test, this._framework) this._tests[fullTitle] = { uuid: hookUUID, startedAt: (new Date()).toISOString() } this.setCurrentHook({ uuid: hookUUID }) this.attachHookData(context, hookUUID) this.listener.hookStarted(this.getRunData(test, 'HookRunStarted')) } async afterHook (test: Frameworks.Test|CucumberHook|undefined, result: Frameworks.TestResult) { if (!frameworkSupportsHook('after', this._framework)) { return } if (this._framework === 'cucumber') { await this.processCucumberHook(test as CucumberHook|undefined, { event: 'after' }, result) return } test = test as Frameworks.Test const fullTitle = getUniqueIdentifier(test as Frameworks.Test, this._framework) if (this._tests[fullTitle]) { this._tests[fullTitle].finishedAt = (new Date()).toISOString() } else { this._tests[fullTitle] = { finishedAt: (new Date()).toISOString() } } this.setCurrentHook({ uuid: this._tests[fullTitle].uuid, finished: true }) this.listener.hookFinished(this.getRunData(test, 'HookRunFinished', result)) const hookType = getHookType(test.title) /* If any of the `beforeAll`, `beforeEach`, `afterEach` then the tests after the hook won't run in mocha (https://github.com/mochajs/mocha/issues/4392) So if any of this hook fails, then we are sending the next tests in the suite as skipped. This won't be needed for `afterAll`, as even if `afterAll` fails all the tests that we need are already run by then, so we don't need to send the stats for them separately */ if (!result.passed && (hookType === 'BEFORE_EACH' || hookType === 'BEFORE_ALL' || hookType === 'AFTER_EACH')) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const sendTestSkip = async (skippedTest: any) => { // We only need to send the tests that whose state is not determined yet. The state of tests which is determined will already be sent. if (skippedTest.state === undefined) { const fullTitle = `${skippedTest.parent.title} - ${skippedTest.title}` this._tests[fullTitle] = { uuid: uuidv4(), startedAt: (new Date()).toISOString(), finishedAt: (new Date()).toISOString() } this.listener.testFinished(this.getRunData(skippedTest, 'TestRunSkipped')) } } /* Recursively send the tests as skipped for all suites below the hook. This is to handle nested describe blocks */ const sendSuiteSkipped = async (suite: { tests: unknown[], suites: unknown[] }) => { for (const skippedTest of suite.tests) { await sendTestSkip(skippedTest) } for (const skippedSuite of suite.suites) { // @ts-expect-error fix types here await sendSuiteSkipped(skippedSuite) } } await sendSuiteSkipped(test.ctx.test.parent) } } public getHookRunDataForCucumber(hookData: TestMeta, eventType: string, result?: Frameworks.TestResult) { const { uri, feature } = this._cucumberData const testData: TestData = { uuid: hookData.uuid, type: 'hook', name: this.getCucumberHookName(hookData.hookType), body: { lang: 'webdriverio', code: null }, started_at: hookData.startedAt, finished_at: hookData.finishedAt, hook_type: hookData.hookType, test_run_id: hookData.testRunId, scope: feature?.name, scopes: [feature?.name || ''], file_name: uri ? path.relative(process.cwd(), uri) : undefined, location: uri ? path.relative(process.cwd(), uri) : undefined, vc_filepath: (this._gitConfigPath && uri) ? path.relative(this._gitConfigPath, uri) : undefined, result: 'pending', framework: this._framework } if (eventType === 'HookRunFinished' && result) { testData.result = result.passed ? 'passed' : 'failed' testData.retries = result.retries testData.duration_in_ms = result.duration if (!result.passed) { Object.assign(testData, getFailureObject(result.error)) } } if (eventType === 'HookRunStarted') { testData.integrations = {} if (this._browser && this._platformMeta) { const provider = getCloudProvider(this._browser) testData.integrations[provider] = this.getIntegrationsObject() } } return testData } async beforeTest (test: Frameworks.Test) { const uuid = uuidv4() InsightsHandler.currentTest = { test, uuid } if (this._framework !== 'mocha') { return } const fullTitle = getUniqueIdentifier(test, this._framework) this._tests[fullTitle] = { uuid, startedAt: (new Date()).toISOString() } this.listener.testStarted(this.getRunData(test, 'TestRunStarted')) } async afterTest (test: Frameworks.Test, result: Frameworks.TestResult) { if (this._framework !== 'mocha') { return } const fullTitle = getUniqueIdentifier(test, this._framework) this._tests[fullTitle] = { ...(this._tests[fullTitle] || {}), finishedAt: (new Date()).toISOString() } this.flushCBTDataQueue() const testData = this.getRunData(test, 'TestRunFinished', result) this.listener.testFinished(testData) const testFinishHashCode = generateHashCodeFromFields( [ testData.integrations?.browserstack?.browser ?? '', testData.integrations?.browserstack?.browser_version ?? '', testData.integrations?.browserstack?.platform ?? '', testData.integrations?.browserstack?.session_id ?? '', testData.integrations?.capabilities ?? {}, testData.file_name ?? '', testData.scopes ?? [], testData.name ?? '' ] ) TestReporter.hashCodeToHandleTestSkip[testFinishHashCode] = testData.uuid ?? '' } /** * Cucumber Only */ async beforeFeature(uri: string, feature: Feature) { this._cucumberData.scenariosStarted = false this._cucumberData.feature = feature this._cucumberData.uri = uri } async beforeScenario (world: ITestCaseHookParameter) { const uuid = uuidv4() InsightsHandler.currentTest = { uuid } this._cucumberData.scenario = world.pickle this._cucumberData.scenariosStarted = true this._cucumberData.stepsStarted = false const pickleData = world.pickle const gherkinDocument = world.gherkinDocument const featureData = gherkinDocument.feature const uniqueId = getUniqueIdentifierForCucumber(world) const testMetaData: TestMeta = { uuid: uuid, startedAt: (new Date()).toISOString() } if (pickleData) { testMetaData.scenario = { name: pickleData.name, } } if (gherkinDocument && featureData) { testMetaData.feature = { path: gherkinDocument.uri, name: featureData.name, description: featureData.description, } } this._tests[uniqueId] = testMetaData this.listener.testStarted(this.getTestRunDataForCucumber(world, 'TestRunStarted')) } async afterScenario (world: ITestCaseHookParameter) { this._cucumberData.scenario = undefined this.flushCBTDataQueue() this.listener.testFinished(this.getTestRunDataForCucumber(world, 'TestRunFinished')) } async beforeStep (step: Frameworks.PickleStep, scenario: Pickle) { this._cucumberData.stepsStarted = true this._cucumberData.steps.push(step) const uniqueId = getUniqueIdentifierForCucumber({ pickle: scenario } as ITestCaseHookParameter) const testMetaData = this._tests[uniqueId] || { steps: [] } if (testMetaData && !testMetaData.steps) { testMetaData.steps = [] } testMetaData.steps?.push({ id: step.id, text: step.text, keyword: step.keyword, started_at: (new Date()).toISOString() }) this._tests[uniqueId] = testMetaData } async afterStep (step: Frameworks.PickleStep, scenario: Pickle, result: Frameworks.PickleResult) { this._cucumberData.steps.pop() const uniqueId = getUniqueIdentifierForCucumber({ pickle: scenario } as ITestCaseHookParameter) const testMetaData = this._tests[uniqueId] || { steps: [] } if (!testMetaData.steps) { testMetaData.steps = [{ id: step.id, text: step.text, keyword: step.keyword, finished_at: (new Date()).toISOString(), result: result.passed ? 'PASSED' : 'FAILED', duration: result.duration, failure: result.error ? removeAnsiColors(result.error) : result.error }] } const stepDetails = testMetaData.steps?.find(item => item.id === step.id) if (stepDetails) { stepDetails.finished_at = (new Date()).toISOString() stepDetails.result = result.passed ? 'PASSED' : 'FAILED' stepDetails.duration = result.duration stepDetails.failure = result.error ? removeAnsiColors(result.error) : result.error } this._tests[uniqueId] = testMetaData } /** * misc methods */ appendTestItemLog = async (stdLog: StdLog) => { try { if (this._currentHook.uuid && !this._currentHook.finished && (this._framework === 'mocha' || this._framework === 'cucumber')) { stdLog.hook_run_uuid = this._currentHook.uuid } else if (InsightsHandler.currentTest.uuid && (this._framework === 'mocha' || this._framework === 'cucumber')) { stdLog.test_run_uuid = InsightsHandler.currentTest.uuid } if (stdLog.hook_run_uuid || stdLog.test_run_uuid) { this.listener.logCreated([stdLog]) } } catch (error) { BStackLogger.debug(`Exception in uploading log data to Observability with error : ${error}`) } } async browserCommand (commandType: string, args: BeforeCommandArgs | AfterCommandArgs, test?: Frameworks.Test | ITestCaseHookParameter) { const dataKey = `${args.sessionId}_${args.method}_${args.endpoint}` if (commandType === 'client:beforeCommand') { this._commands[dataKey] = args return } if (!test) { return } const identifier = this.getIdentifier(test) const testMeta = this._tests[identifier] || TestReporter.getTests()[identifier] if (!testMeta) { return } // log screenshot const body = 'body' in args ? args.body : undefined const result = 'result' in args ? args.result as { value: string } : undefined if (Boolean(process.env[TESTOPS_SCREENSHOT_ENV]) && isScreenshotCommand(args) && result?.value) { await this.listener.onScreenshot([{ test_run_uuid: testMeta.uuid, timestamp: new Date().toISOString(), message: result.value, kind: 'TEST_SCREENSHOT' }]) } const requestData = this._commands[dataKey] if (!requestData) { return } // log http request this.listener.logCreated([{ test_run_uuid: testMeta.uuid, timestamp: new Date().toISOString(), kind: 'HTTP', http_response: { path: requestData.endpoint, method: requestData.method, body, response: result } }] ) } /* * private methods */ /** * Check if any test steps failed (excluding hook failures) * This is used when ignoreHooksStatus is true to determine test status based only on test steps */ public hasTestStepFailures(world: ITestCaseHookParameter): boolean { if (!world?.pickle) { return false } const uniqueId = getUniqueIdentifierForCucumber(world) const testMetaData = this._tests[uniqueId] if (!testMetaData?.steps) { return false } // Check if any step failed return testMetaData.steps.some(step => step.result === 'FAILED') } // eslint-disable-next-line @typescript-eslint/no-explicit-any private attachHookData (context: any, hookId: string): void { if (context.currentTest && context.currentTest.parent) { const parentTest = `${context.currentTest.parent.title} - ${context.currentTest.title}` if (!this._hooks[parentTest]) { this._hooks[parentTest] = [] } this._hooks[parentTest].push(hookId) return } else if (context.test) { this.setHooksFromSuite(context.test.parent, hookId) } } // eslint-disable-next-line @typescript-eslint/no-explicit-any private setHooksFromSuite(parent: any, hookId: string): boolean { if (!parent) { return false } if (parent.tests && parent.tests.length > 0) { const uniqueIdentifier = getUniqueIdentifier(parent.tests[0], this._framework) if (!this._hooks[uniqueIdentifier]) { this._hooks[uniqueIdentifier] = [] } this._hooks[uniqueIdentifier].push(hookId) return true } for (const suite of parent.suites) { const result = this.setHooksFromSuite(suite, hookId) if (result) { return true } } return false } /* * Get hierarchy info */ private getHierarchy (test: Frameworks.Test) { const value: string[] = [] if (test.ctx && test.ctx.test) { // If we already have the parent object, utilize it else get from context let parent = typeof test.parent === 'object' ? test.parent : test.ctx.test.parent while (parent && parent.title !== '') { value.push(parent.title) parent = parent.parent } } else if (test.description && test.fullName) { // for Jasmine value.push(test.description) value.push(test.fullName.replace(new RegExp(' ' + test.description + '$'), '')) } return value.reverse() } private getRunData (test: Frameworks.Test, eventType: string, results?: Frameworks.TestResult) { const fullTitle = getUniqueIdentifier(test, this._framework) const testMetaData = this._tests[fullTitle] const filename = test.file || this._suiteFile this.currentTestId = testMetaData.uuid if (eventType === 'TestRunStarted') { InsightsHandler.currentTest.name = test.title || test.description } const testData: TestData = { uuid: testMetaData.uuid, type: test.type || 'test', name: test.title || test.description, body: { lang: 'webdriverio', code: test.body }, scope: fullTitle, scopes: this.getHierarchy(test), identifier: fullTitle, file_name: filename ? path.relative(process.cwd(), filename) : undefined, location: filename ? path.relative(process.cwd(), filename) : undefined, vc_filepath: (this._gitConfigPath && filename) ? path.relative(this._gitConfigPath, filename) : undefined, started_at: testMetaData.startedAt, finished_at: testMetaData.finishedAt, result: 'pending', framework: this._framework } if ((eventType === 'TestRunFinished' || eventType === 'HookRunFinished') && results) { testData.integrations = {} if (this._browser && this._platformMeta) { const provider = getCloudProvider(this._browser) testData.integrations[provider] = this.getIntegrationsObject() } const { error, passed } = results if (!passed) { testData.result = (error && error.message && error.message.includes('sync skip; aborting execution')) ? 'ignore' : 'failed' if (error && testData.result !== 'skipped') { testData.failure = [{ backtrace: [removeAnsiColors(error.message), removeAnsiColors(error.stack || '')] }] // add all errors here testData.failure_reason = removeAnsiColors(error.message) testData.failure_type = isUndefined(error.message) ? null : error.message.toString().match(/AssertionError/) ? 'AssertionError' : 'UnhandledError' //verify if this is working } } else { testData.result = 'passed' } testData.retries = results.retries testData.duration_in_ms = results.duration if (this._hooks[fullTitle]) { testData.hooks = this._hooks[fullTitle] } } if (eventType === 'TestRunStarted' || eventType === 'TestRunSkipped' || eventType === 'HookRunStarted') { testData.integrations = {} if (this._browser && this._platformMeta) { const provider = getCloudProvider(this._browser) testData.integrations[provider] = this.getIntegrationsObject() } } if (eventType === 'TestRunSkipped') { if (this._hooks[fullTitle]) { testData.hooks = this._hooks[fullTitle] } testData.result = 'skipped' eventType = 'TestRunFinished' } /* istanbul ignore if */ if (eventType.match(/HookRun/)) { testData.hook_type = testData.name?.toLowerCase() ? getHookType(testData.name.toLowerCase()) : 'undefined' testData.test_run_id = this.getTestRunId(test.ctx) } return testData } // eslint-disable-next-line @typescript-eslint/no-explicit-any private getTestRunId(context: any): string|undefined { if (!context) { return } if (context.currentTest) { const uniqueIdentifier = getUniqueIdentifier(context.currentTest, this._framework) return this._tests[uniqueIdentifier] && this._tests[uniqueIdentifier].uuid } if (!context.test) { return } return this.getTestRunIdFromSuite(context.test.parent) } // eslint-disable-next-line @typescript-eslint/no-explicit-any private getTestRunIdFromSuite(parent: any): string|undefined { if (!parent) { return } for (const test of parent.tests) { const uniqueIdentifier = getUniqueIdentifier(test, this._framework) if (this._tests[uniqueIdentifier]) { return this._tests[uniqueIdentifier].uuid } } for (const suite of parent.suites) { const testRunId: string|undefined = this.getTestRunIdFromSuite(suite) if (testRunId) { return testRunId } } return } private getTestRunDataForCucumber (worldObj: ITestCaseHookParameter|null, eventType: string, testMetaData: TestMeta|null = null) { const world: ITestCaseHookParameter = worldObj as ITestCaseHookParameter const dataHub = testMetaData ? testMetaData : (this._tests[getUniqueIdentifierForCucumber((world as ITestCaseHookParameter))] || {}) const { feature, scenario, steps, uuid, startedAt, finishedAt } = dataHub const examples = !testMetaData ? getScenarioExamples(world as ITestCaseHookParameter) : undefined let fullNameWithExamples: string if (!testMetaData) { fullNameWithExamples = examples ? world.pickle.name + ' (' + examples.join(', ') + ')' : world.pickle.name } else { fullNameWithExamples = scenario?.name || '' } this.currentTestId = uuid if (eventType === 'TestRunStarted') { InsightsHandler.currentTest.name = fullNameWithExamples } const testData: TestData = { uuid: uuid, started_at: startedAt, finished_at: finishedAt, type: 'test', body: { lang: 'webdriverio', code: null }, name: fullNameWithExamples, scope: fullNameWithExamples, scopes: [feature?.name || ''], identifier: scenario?.name, file_name: feature && feature.path ? path.relative(process.cwd(), feature.path) : undefined, location: feature && feature.path ? path.relative(process.cwd(), feature.path) : undefined, vc_filepath: (this._gitConfigPath && feature?.path) ? path.relative(this._gitConfigPath, feature?.path) : undefined, framework: this._framework, result: 'pending', meta: { feature: feature, scenario: scenario, steps: steps, examples: examples } } if (eventType === 'TestRunStarted' || eventType === 'TestRunSkipped') { testData.integrations = {} if (this._browser && this._platformMeta) { const provider = getCloudProvider(this._browser) testData.integrations[provider] = this.getIntegrationsObject() } } /* istanbul ignore if */ if (world?.result) { let result = world.result.status.toLowerCase() if (result !== 'passed' && result !== 'failed') { result = 'skipped' // mark UNKNOWN/UNDEFINED/AMBIGUOUS/PENDING as skipped } // Handle ignoreHooksStatus: when enabled and scenario failed, check if it's due to hook failures only const ignoreHooksStatus = this._options?.testObservabilityOptions?.ignoreHooksStatus === true if (ignoreHooksStatus && result === 'failed' && world) { // Check if any test steps failed (excluding hook failures) const hasTestStepFailures = this.hasTestStepFailures(world) if (!hasTestStepFailures) { // Only hooks failed, override result to passed for Test Observability result = 'passed' } } testData.finished_at = (new Date()).toISOString() testData.result = result testData.duration_in_ms = world.result.duration.seconds * 1000 + world.result.duration.nanos / 1000000 // send duration in ms if (result === 'failed') { testData.failure = [ { 'backtrace': [world.result.message ? removeAnsiColors(world.result.message) : 'unknown'] } ] testData.failure_reason = world.result.message ? removeAnsiColors(world.result.message) : world.result.message if (world.result.message) { testData.failure_type = world.result.message.match(/AssertionError/) ? 'AssertionError' : 'UnhandledError' } } } if (world?.pickle) { testData.tags = world.pickle.tags.map( ({ name }: { name: string }) => (name) ) } if (eventType === 'TestRunSkipped') { testData.result = 'skipped' } return testData } public async flushCBTDataQueue() { if (isUndefined(this.currentTestId)) {return} this.cbtQueue.forEach(cbtData => { cbtData.uuid = this.currentTestId! this.listener.cbtSessionCreated(cbtData) }) this.currentTestId = undefined // set undefined for next test } async sendCBTInfo() { const integrationsData: Record = {} if (this._browser && this._platformMeta) { const provider = getCloudProvider(this._browser) as keyof IntegrationObject integrationsData[provider] = this.getIntegrationsObject() } const cbtData: CBTData = { uuid: '', integrations: integrationsData } if (this.currentTestId !== undefined) { cbtData.uuid = this.currentTestId this.listener.cbtSessionCreated(cbtData) } else { this.cbtQueue.push(cbtData) } } private getIntegrationsObject () { const caps = (this._browser as WebdriverIO.Browser)?.capabilities as WebdriverIO.Capabilities const sessionId = (this._browser as WebdriverIO.Browser)?.sessionId return { capabilities: caps, session_id: sessionId, browser: caps?.browserName, browser_version: caps?.browserVersion, platform: caps?.platformName, product: this._platformMeta?.product, platform_version: getPlatformVersion(caps, this._userCaps as WebdriverIO.Capabilities) } } private getIdentifier (test: Frameworks.Test | ITestCaseHookParameter) { if ('pickle' in test) { return getUniqueIdentifierForCucumber(test) } return getUniqueIdentifier(test, this._framework) } } // https://github.com/microsoft/TypeScript/issues/6543 const InsightsHandler: typeof _InsightsHandler = o11yClassErrorHandler(_InsightsHandler) type InsightsHandler = _InsightsHandler export default InsightsHandler