UNPKG

@ericmconnelly/wdio-slack-reporter

Version:

Reporter from WebdriverIO using Web API to send results to Slack.

1,137 lines (1,051 loc) 34.8 kB
import { Block, ChatPostMessageArguments, FilesUploadArguments, KnownBlock, WebAPICallResult, WebClient, } from '@slack/web-api'; import { IncomingWebhook, IncomingWebhookResult, IncomingWebhookSendArguments, } from '@slack/webhook'; import getLogger from '@wdio/logger'; import WDIOReporter, { HookStats, RunnerStats, SuiteStats, TestStats, } from '@wdio/reporter'; import { Capabilities } from '@wdio/types'; import util from 'util'; import { DEFAULT_COLOR, DEFAULT_INDENT, EMOJI_SYMBOLS, FAILED_COLOR, SLACK_ICON_URL, SLACK_NAME, ERROR_MESSAGES, EVENTS, SLACK_REQUEST_TYPE, FINISHED_COLOR, } from './constants.js'; import { SlackRequestType, SlackReporterOptions, EmojiSymbols, StateCount, CucumberStats, TestResultType, } from './types.js'; const log = getLogger('@moroo/wdio-slack-reporter'); class SlackReporter extends WDIOReporter { private static resultsUrl?: string; private _slackRequestQueue: SlackRequestType[] = []; private _lastSlackWebAPICallResult?: WebAPICallResult; private _pendingSlackRequestCount = 0; private _stateCounts: StateCount = { passed: 0, failed: 0, skipped: 0, }; private _client?: WebClient; private _webhook?: IncomingWebhook; private _channel?: string; private _symbols: EmojiSymbols; private _isCucumberFramework: boolean = false; private _title?: string; private _notifyTestStartMessage: boolean = true; private _notifyFailedCase: boolean = true; private _uploadScreenshotOfFailedCase: boolean = true; private _notifyTestFinishMessage: boolean = true; private _notifyDetailResultThread: boolean = true; private _filterForDetailResults: TestResultType[] = [ 'passed', 'failed', 'pending', 'skipped', ]; private _isSynchronizing: boolean = false; private _interval: NodeJS.Timeout; private _hasRunnerEnd = false; private _lastScreenshotBuffer?: Buffer = undefined; private _suiteUids = new Set<string>(); private _orderedSuites: SuiteStats[] = []; private _cucumberOrderedTests: CucumberStats[] = []; private _indents: number = 0; private _suiteIndents: Record<string, number> = {}; private _currentSuite?: SuiteStats; constructor(options: SlackReporterOptions) { super(Object.assign({ stdout: true }, options)); if (!options.slackOptions) { log.error(ERROR_MESSAGES.UNDEFINED_SLACK_OPTION); log.debug(options.slackOptions); throw new Error(ERROR_MESSAGES.UNDEFINED_SLACK_OPTION); } if (options.slackOptions.type === 'web-api') { this._client = new WebClient(options.slackOptions.slackBotToken); log.info('Created Slack Web API Client Instance.'); log.debug('Slack Web API Client', { token: options.slackOptions.slackBotToken, channel: options.slackOptions.channel, }); this._channel = options.slackOptions.channel; if (options.slackOptions.notifyDetailResultThread !== undefined) { if (options.notifyTestFinishMessage === false) { log.warn( 'Notify is not possible. because the notifyResultMessage option is off.' ); } this._notifyDetailResultThread = options.slackOptions.notifyDetailResultThread; } if (options.slackOptions.filterForDetailResults !== undefined) { if (options.slackOptions.notifyDetailResultThread === false) { log.warn( 'Detail result filters does not work. because the notifyDetailResultThread option is off.' ); } if (options.slackOptions.filterForDetailResults.length === 0) { log.info( 'If there are no filters (array is empty), all filters are applied.' ); } else { this._filterForDetailResults = [ ...options.slackOptions.filterForDetailResults, ]; } } if (options.slackOptions.uploadScreenshotOfFailedCase !== undefined) { this._uploadScreenshotOfFailedCase = options.slackOptions.uploadScreenshotOfFailedCase; } if (options.slackOptions.uploadScreenshotOfFailedCase !== undefined) { this._uploadScreenshotOfFailedCase = options.slackOptions.uploadScreenshotOfFailedCase; } if (options.slackOptions.createScreenshotPayload) { this.createScreenshotPayload = options.slackOptions.createScreenshotPayload.bind(this); log.info('The [createScreenshotPayload] function has been overridden.'); log.debug('RESULT', this.createScreenshotPayload.toString()); } if (options.slackOptions.createResultDetailPayload) { this.createResultDetailPayload = options.slackOptions.createResultDetailPayload.bind(this); log.info( 'The [createResultDetailPayload] function has been overridden.' ); log.debug('RESULT', this.createResultDetailPayload.toString()); } } else { this._webhook = new IncomingWebhook(options.slackOptions.webhook, { username: options.slackOptions.slackName || SLACK_NAME, icon_url: options.slackOptions.slackIconUrl || SLACK_ICON_URL, }); log.info('Created Slack Webhook Instance.'); log.debug('IncomingWebhook', { webhook: options.slackOptions.webhook, username: options.slackOptions.slackName || SLACK_NAME, icon_url: options.slackOptions.slackIconUrl || SLACK_ICON_URL, }); } this._symbols = { passed: options.emojiSymbols?.passed || EMOJI_SYMBOLS.PASSED, skipped: options.emojiSymbols?.skipped || EMOJI_SYMBOLS.SKIPPED, failed: options.emojiSymbols?.failed || EMOJI_SYMBOLS.FAILED, pending: options.emojiSymbols?.pending || EMOJI_SYMBOLS.PENDING, start: options.emojiSymbols?.start || EMOJI_SYMBOLS.ROKET, finished: options.emojiSymbols?.finished || EMOJI_SYMBOLS.CHECKERED_FLAG, watch: options.emojiSymbols?.watch || EMOJI_SYMBOLS.STOPWATCH, }; this._title = options.title; if (options.resultsUrl !== undefined) { SlackReporter.setResultsUrl(options.resultsUrl); } if (options.notifyTestStartMessage !== undefined) { this._notifyTestStartMessage = options.notifyTestStartMessage; } if (options.notifyFailedCase !== undefined) { this._notifyFailedCase = options.notifyFailedCase; } if (options.notifyTestFinishMessage !== undefined) { this._notifyTestFinishMessage = options.notifyTestFinishMessage; } this._interval = global.setInterval(this.sync.bind(this), 100); if (options.createStartPayload) { this.createStartPayload = options.createStartPayload.bind(this); log.info('The [createStartPayload] function has been overridden.'); log.debug('RESULT', this.createStartPayload.toString()); } if (options.createFailedTestPayload) { this.createFailedTestPayload = options.createFailedTestPayload.bind(this); log.info('The [createFailedTestPayload] function has been overridden.'); log.debug('RESULT', this.createFailedTestPayload.toString()); } if (options.createResultPayload) { this.createResultPayload = options.createResultPayload.bind(this); log.info('The [createResultPayload] function has been overridden.'); log.debug('RESULT', this.createResultPayload.toString()); } process.on(EVENTS.POST_MESSAGE, this.postMessage.bind(this)); process.on(EVENTS.UPLOAD, this.upload.bind(this)); process.on(EVENTS.SEND, this.send.bind(this)); process.on(EVENTS.SCREENSHOT, this.uploadFailedTestScreenshot.bind(this)); } static getResultsUrl(): string | undefined { return SlackReporter.resultsUrl; } static setResultsUrl(url: string | undefined): void { SlackReporter.resultsUrl = url; } /** * Upload failed test scrteenshot * @param {WebdriverIO.Browser} browser Parameters used by WebdriverIO.Browser * @param {{page: Page, options: ScreenshotOptions}} puppeteer Parameters used by Puppeteer * @return {Promise<Buffer>} */ static uploadFailedTestScreenshot(data: string | Buffer): void { let buffer: Buffer; if (typeof data === 'string') { buffer = Buffer.from(data, 'base64'); } else { buffer = data; } process.emit(EVENTS.SCREENSHOT, buffer); } /** * Post message from Slack web-api * @param {ChatPostMessageArguments} payload Parameters used by Slack web-api * @return {Promise<WebAPICallResult>} */ static postMessage( payload: ChatPostMessageArguments ): Promise<WebAPICallResult> { return new Promise((resolve, reject) => { process.emit(EVENTS.POST_MESSAGE, payload); process.once(EVENTS.RESULT, ({ result, error }) => { if (result) { resolve(result as WebAPICallResult); } reject(error); }); }); } /** * Upload from Slack web-api * @param {FilesUploadArguments} payload Parameters used by Slack web-api * @return {WebAPICallResult} */ static async upload( payload: FilesUploadArguments ): Promise<WebAPICallResult> { return new Promise((resolve, reject) => { process.emit(EVENTS.UPLOAD, payload); process.once(EVENTS.RESULT, ({ result, error }) => { if (result) { resolve(result as WebAPICallResult); } reject(error); }); }); } /** * Send from Slack webhook * @param {IncomingWebhookSendArguments} payload Parameters used by Slack webhook * @return {IncomingWebhookResult} */ static async send( payload: IncomingWebhookSendArguments ): Promise<IncomingWebhookResult> { return new Promise((resolve, reject) => { process.emit(EVENTS.SEND, payload); process.once(EVENTS.RESULT, ({ result, error }) => { if (result) { resolve(result as IncomingWebhookResult); } reject(error); }); }); } private uploadFailedTestScreenshot(buffer: Buffer): void { if (this._client) { if (this._notifyFailedCase && this._uploadScreenshotOfFailedCase) { this._lastScreenshotBuffer = buffer; return; } else { log.warn(ERROR_MESSAGES.DISABLED_OPTIONS); } } else { log.warn(ERROR_MESSAGES.NOT_USING_WEB_API); } // return new Promise((resolve, reject) => { // const interval = setInterval(() => { // if (this._lastScreenshotBuffer === undefined) { // clearInterval(interval); // if (this._client && this._notifyFailedCase) { // this._lastScreenshotBuffer = buffer; // } else { // log.warn( // ERROR_MESSAGES.NOT_USING_WEB_API_OR_DISABLED_NOTIFY_FAILED_CASE // ); // } // resolve(); // } // }, 100); // }); } private async postMessage( payload: ChatPostMessageArguments ): Promise<WebAPICallResult> { if (this._client) { try { log.debug('COMMAND', `postMessage(${payload})`); this._pendingSlackRequestCount++; const result = await this._client.chat.postMessage(payload); log.debug('RESULT', util.inspect(result)); process.emit(EVENTS.RESULT, { result, error: undefined }); return result; } catch (error) { log.error(error); process.emit(EVENTS.RESULT, { result: undefined, error }); throw error; } finally { this._pendingSlackRequestCount--; } } log.error(ERROR_MESSAGES.NOT_USING_WEB_API); throw new Error(ERROR_MESSAGES.NOT_USING_WEB_API); } private async upload( payload: FilesUploadArguments ): Promise<WebAPICallResult> { if (this._client) { try { log.debug('COMMAND', `upload(${payload})`); this._pendingSlackRequestCount++; const result = await this._client.files.upload(payload); log.debug('RESULT', util.inspect(result)); process.emit(EVENTS.RESULT, { result, error: undefined }); return result; } catch (error) { log.error(error); process.emit(EVENTS.RESULT, { result: undefined, error }); throw error; } finally { this._pendingSlackRequestCount--; } } log.error(ERROR_MESSAGES.NOT_USING_WEB_API); throw new Error(ERROR_MESSAGES.NOT_USING_WEB_API); } private async send( payload: IncomingWebhookSendArguments ): Promise<IncomingWebhookResult> { if (this._webhook) { try { log.debug('COMMAND', `send(${payload})`); this._pendingSlackRequestCount++; const result = await this._webhook.send(payload); log.debug('RESULT', util.inspect(result)); process.emit(EVENTS.RESULT, { result, error: undefined }); return result; } catch (error) { log.error(error); process.emit(EVENTS.RESULT, { result: undefined, error }); throw error; } finally { this._pendingSlackRequestCount--; } } log.error(ERROR_MESSAGES.NOT_USING_WEBHOOK); throw new Error(ERROR_MESSAGES.NOT_USING_WEBHOOK); } get isSynchronised(): boolean { return ( this._pendingSlackRequestCount === 0 && this._isSynchronizing === false ); } private async sync(): Promise<void> { if ( this._hasRunnerEnd && this._slackRequestQueue.length === 0 && this._pendingSlackRequestCount === 0 ) { clearInterval(this._interval); } if ( this._isSynchronizing || this._slackRequestQueue.length === 0 || this._pendingSlackRequestCount > 0 ) { return; } try { this._isSynchronizing = true; log.info('Start Synchronising...'); await this.next(); } catch (error) { log.error(error); throw error; } finally { this._isSynchronizing = false; log.info('End Synchronising!!!'); } } private async next() { const request = this._slackRequestQueue.shift(); let result: WebAPICallResult | IncomingWebhookResult; log.info('POST', `Slack Request ${request?.type}`); log.debug('DATA', util.inspect(request?.payload)); if (request) { try { this._pendingSlackRequestCount++; switch (request.type) { case SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE: { if (this._client) { result = await this._client.chat.postMessage({ ...request.payload, thread_ts: request.isDetailResult ? (this._lastSlackWebAPICallResult?.ts as string) : undefined, }); this._lastSlackWebAPICallResult = result; log.debug('RESULT', util.inspect(result)); } break; } case SLACK_REQUEST_TYPE.WEB_API_UPLOAD: { if (this._client) { result = await this._client.files.upload({ ...request.payload, thread_ts: this._lastSlackWebAPICallResult?.ts as string, }); log.debug('RESULT', util.inspect(result)); } break; } case SLACK_REQUEST_TYPE.WEBHOOK_SEND: { if (this._webhook) { result = await this._webhook.send(request.payload); log.debug('RESULT', util.inspect(result)); } break; } } } catch (error) { log.error(error); } finally { this._pendingSlackRequestCount--; } if (this._slackRequestQueue.length > 0) { await this.next(); } } } private convertErrorStack(stack: string): string { return stack.replace( // eslint-disable-next-line no-control-regex /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '' ); } private getEnviromentCombo( capability: Capabilities.RemoteCapability, isMultiremote = false ): string { let output = ''; const capabilities: Capabilities.RemoteCapability = ((capability as Capabilities.W3CCapabilities) .alwaysMatch as Capabilities.DesiredCapabilities) || (capability as Capabilities.DesiredCapabilities); const drivers: { driverName?: string; capability: Capabilities.RemoteCapability; }[] = []; if (isMultiremote) { output += '*MultiRemote*: \n'; Object.keys(capabilities).forEach((key) => { drivers.push({ driverName: key, capability: (capabilities as Capabilities.MultiRemoteCapabilities)[ key ], }); }); } else { drivers.push({ capability: capabilities, }); } drivers.forEach(({ driverName, capability }, index, array) => { const isLastIndex = array.length - 1 === index; let env = ''; const caps = ((capability as Capabilities.W3CCapabilities) .alwaysMatch as Capabilities.DesiredCapabilities) || (capability as Capabilities.DesiredCapabilities); const device = caps.deviceName; const browser = caps.browserName || caps.browser; const version = caps.browserVersion || caps.version || // eslint-disable-next-line @typescript-eslint/no-explicit-any (caps as any).platformVersion || caps.browser_version; const platform = caps.platformName || caps.platform || (caps.os ? caps.os + (caps.os_version ? ` ${caps.os_version}` : '') : '(unknown)'); if (device) { const program = // eslint-disable-next-line @typescript-eslint/no-explicit-any ((caps as any).app || '').replace('sauce-storage:', '') || caps.browserName; const executing = program ? `executing ${program}` : ''; env = `${device} on ${platform} ${version} ${executing}`.trim(); } else { env = browser + (version ? ` (v${version})` : '') + ` on ${platform}`; } output += isMultiremote ? `- *${driverName}*: ` : '*Driver*: '; output += env; output += isLastIndex ? '' : '\n'; }); return output; } /** * Indent a suite based on where how it's nested * @param {String} uid Unique suite key * @return {String} Spaces for indentation */ private indent(uid: string): string { const indents = this._suiteIndents[uid]; return indents === 0 ? '' : Array(indents).join(DEFAULT_INDENT); } /** * Indent a suite based on where how it's nested * @param {StateCount} stateCounts Stat count * @return {String} String to the stat count to be displayed in Slack */ private getCounts(stateCounts: StateCount): string { return `*${this._symbols.passed} Passed: ${stateCounts.passed} | ${this._symbols.failed} Failed: ${stateCounts.failed} | ${this._symbols.skipped} Skipped: ${stateCounts.skipped}*`; } private createStartPayload( runnerStats: RunnerStats ): ChatPostMessageArguments | IncomingWebhookSendArguments { const text = `${ this._title ? '*Title*: `' + this._title + '`\n' : '' }${this.getEnviromentCombo( runnerStats.capabilities, runnerStats.isMultiremote )}`; const payload: ChatPostMessageArguments | IncomingWebhookSendArguments = { channel: this._channel, text: `${this._symbols.start} Start testing${ this._title ? 'for ' + this._title : '' }`, blocks: [ { type: 'header', text: { type: 'plain_text', text: `${this._symbols.start} Start testing`, emoji: true, }, }, ], attachments: [ { color: DEFAULT_COLOR, text, ts: Date.now().toString(), }, ], }; return payload; } private createFailedTestPayload( hookAndTest: HookStats | TestStats ): ChatPostMessageArguments | IncomingWebhookSendArguments { const stack = hookAndTest.error?.stack ? '```' + this.convertErrorStack(hookAndTest.error.stack) + '```' : ''; const payload: ChatPostMessageArguments | IncomingWebhookSendArguments = { channel: this._channel, text: `${this._symbols.failed} Test failure`, blocks: [ { type: 'header', text: { type: 'plain_text', text: `${this._symbols.failed} Test failure`, emoji: true, }, }, ], attachments: [ { color: FAILED_COLOR, title: `${ this._currentSuite ? this._currentSuite.title : hookAndTest.parent }`, text: `* » ${hookAndTest.title}*\n${stack}`, }, ], }; return payload; } private createScreenshotPayload( testStats: TestStats, screenshotBuffer: Buffer ): FilesUploadArguments { const payload: FilesUploadArguments = { channels: this._channel, initial_comment: `Screenshot for Fail to ${testStats.title}`, filename: `${testStats.uid}.png`, filetype: 'png', file: screenshotBuffer, }; return payload; } private createResultPayload( runnerStats: RunnerStats, stateCounts: StateCount ): ChatPostMessageArguments | IncomingWebhookSendArguments { const resltsUrl = SlackReporter.getResultsUrl(); const counts = this.getCounts(stateCounts); const payload: ChatPostMessageArguments | IncomingWebhookSendArguments = { channel: this._channel, text: `${this._symbols.finished} End of test${ this._title ? ' - ' + this._title : '' }\n${counts}`, blocks: [ { type: 'header', text: { type: 'plain_text', text: `${this._symbols.finished} End of test - ${ this._symbols.watch } ${runnerStats.duration / 1000}s`, emoji: true, }, }, ], attachments: [ { color: FINISHED_COLOR, text: `${this._title ? `*Title*: \`${this._title}\`\n` : ''}${ resltsUrl ? `*Results*: <${resltsUrl}>\n` : '' }${counts}`, ts: Date.now().toString(), }, ], }; return payload; } private createResultDetailPayload( runnerStats: RunnerStats, stateCounts: StateCount ): ChatPostMessageArguments | IncomingWebhookSendArguments { const counts = this.getCounts(stateCounts); const payload: ChatPostMessageArguments | IncomingWebhookSendArguments = { channel: this._channel, text: `${this._title ? this._title + '\n' : ''}${counts}`, blocks: [ { type: 'header', text: { type: 'plain_text', text: 'Result Details', emoji: true, }, }, ...this.getResultDetailPayloads(), { type: 'section', text: { type: 'mrkdwn', text: `${counts}\n${this._symbols.watch} ${ runnerStats.duration / 1000 }s`, }, }, { type: 'context', elements: [ { type: 'mrkdwn', text: `*Filter*: ${this._filterForDetailResults .map((filter) => '`' + filter + '`') .join(', ')}`, }, ], }, ], }; return payload; } private getResultDetailPayloads(): (Block | KnownBlock)[] { const output: string[] = []; let suites = this._isCucumberFramework ? this.getOrderedCucumberTests() : this.getOrderedSuites(); const blocks: (Block | KnownBlock)[] = []; // Filter Detailed suites by state (Cucumber only) if (this._isCucumberFramework && this._notifyDetailResultThread) { suites = (suites as CucumberStats[]).filter(({ state }) => this._filterForDetailResults.includes(state) ); } for (const suite of suites) { // Don't do anything if a suite has no tests or sub suites if ( suite.tests.length === 0 && suite.suites.length === 0 && suite.hooks.length === 0 ) { continue; } let eventsToReport = this.getEventsToReport(suite); // Filter Detailed tests results by state (if needed) if ( this._isCucumberFramework === false && this._notifyDetailResultThread ) { eventsToReport = eventsToReport.filter(({ state }) => this._filterForDetailResults.includes(state) ); } if (eventsToReport.length === 0) { continue; } // Get the indent/starting point for this suite const suiteIndent = this.indent(suite.uid); // Display the title of the suite if (suite.type) { output.push(`*${suiteIndent}${suite.title}*`); } // display suite description (Cucumber only) if (suite.description) { output.push( ...suite.description .trim() .split('\n') .map((l) => `${suiteIndent}${l.trim()}`) ); } for (const test of eventsToReport) { const testTitle = test.title; const testState = test.state; const testIndent = `${DEFAULT_INDENT}${suiteIndent}`; // Output for a single test output.push( `*${testIndent}${ testState ? `${this._symbols[testState]} ` : '' }${testTitle}*` ); } // Put a line break after each suite (only if tests exist in that suite) if (eventsToReport.length) { const block: Block | KnownBlock = { type: 'section', text: { type: 'mrkdwn', text: output.join('\n'), }, }; output.length = 0; blocks.push(block); } } if (blocks.length === 0) { const block: Block | KnownBlock = { type: 'section', text: { type: 'mrkdwn', text: '*`No filter Results.`*', }, }; blocks.push(block); } return blocks; } private getOrderedSuites() { if (this._orderedSuites.length) { return this._orderedSuites; } this._orderedSuites = []; for (const uid of this._suiteUids) { for (const [suiteUid, suite] of Object.entries(this.suites)) { if (suiteUid !== uid) { continue; } this._orderedSuites.push(suite); } } return this._orderedSuites; } private getOrderedCucumberTests() { if (this._cucumberOrderedTests.length) { return this._cucumberOrderedTests; } this._cucumberOrderedTests = []; for (const uid of this._suiteUids) { for (const [suiteUid, suite] of Object.entries(this.suites)) { if (suiteUid !== uid) { continue; } if (suite.type === 'scenario') { let testState: CucumberStats['state'] = 'skipped'; if (suite.tests.some((test) => test.state === 'passed')) { testState = 'passed'; } else if (suite.tests.some((test) => test.state === 'failed')) { testState = 'failed'; } this._cucumberOrderedTests.push( Object.assign(suite, { state: testState }) ); } } } return this._cucumberOrderedTests; } private getCucumberTestsCounts() { const suitesData = this.getOrderedCucumberTests(); const suiteStats: StateCount = { passed: suitesData.filter(({ state }) => state === 'passed').length, failed: suitesData.filter(({ state }) => state === 'failed').length, skipped: suitesData.filter(({ state }) => state === 'skipped').length, }; return suiteStats; } /** * returns everything worth reporting from a suite * @param {Object} suite test suite containing tests and hooks * @return {Object[]} list of events to report */ private getEventsToReport(suite: SuiteStats) { return [ /** * report all tests and only hooks that failed */ ...suite.hooksAndTests.filter((item) => { return item.type === 'test' || Boolean(item.error); }), ]; } onRunnerStart(runnerStats: RunnerStats): void { log.info('INFO', `Test Framework: ${runnerStats.config.framework}`); if (runnerStats.config.framework === 'cucumber') { this._isCucumberFramework = true; } if (this._notifyTestStartMessage) { try { if (this._client) { log.info('INFO', `ON RUNNER START: POST MESSAGE`); this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE, payload: this.createStartPayload( runnerStats ) as ChatPostMessageArguments, }); } else if (this._webhook) { log.info('INFO', `ON RUNNER START: SEND`); this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEBHOOK_SEND, payload: this.createStartPayload( runnerStats ) as IncomingWebhookSendArguments, }); } } catch (error) { log.error(error); throw error; } } } // onBeforeCommand(commandArgs: BeforeCommandArgs): void {} // onAfterCommand(commandArgs: AfterCommandArgs): void {} onSuiteStart(suiteStats: SuiteStats): void { this._currentSuite = suiteStats; this._suiteUids.add(suiteStats.uid); if (this._isCucumberFramework) { if (suiteStats.type === 'feature') { this._indents = 0; this._suiteIndents[suiteStats.uid] = this._indents; } } else { this._suiteIndents[suiteStats.uid] = ++this._indents; } } // onHookStart(hookStat: HookStats): void {} onHookEnd(hookStats: HookStats): void { if (hookStats.error) { this._stateCounts.failed++; if (this._notifyFailedCase) { if (this._client) { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE, payload: this.createFailedTestPayload( hookStats ) as ChatPostMessageArguments, }); } else { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEBHOOK_SEND, payload: this.createFailedTestPayload( hookStats ) as ChatPostMessageArguments, }); } } } } // onTestStart(testStats: TestStats): void {} onTestPass(testStats: TestStats): void { this._stateCounts.passed++; } onTestFail(testStats: TestStats): void { this._stateCounts.failed++; if (this._notifyFailedCase) { if (this._client) { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE, payload: this.createFailedTestPayload( testStats ) as ChatPostMessageArguments, }); if (this._uploadScreenshotOfFailedCase && this._lastScreenshotBuffer) { log.error('UID', testStats.uid); this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_UPLOAD, payload: this.createScreenshotPayload( testStats, this._lastScreenshotBuffer ) as FilesUploadArguments, }); this._lastScreenshotBuffer = undefined; } } else { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEBHOOK_SEND, payload: this.createFailedTestPayload( testStats ) as ChatPostMessageArguments, }); } } } // onTestRetry(testStats: TestStats): void {} onTestSkip(testStats: TestStats): void { this._stateCounts.skipped++; } // onTestEnd(testStats: TestStats): void {} onSuiteEnd(suiteStats: SuiteStats): void { this._indents--; } onRunnerEnd(runnerStats: RunnerStats): void { if (this._notifyTestFinishMessage) { const stateCount = this._isCucumberFramework ? this.getCucumberTestsCounts() : this._stateCounts; try { if (this._client) { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE, payload: this.createResultPayload( runnerStats, stateCount ) as ChatPostMessageArguments, }); if (this._notifyDetailResultThread) { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEB_API_POST_MESSAGE, payload: this.createResultDetailPayload( runnerStats, stateCount ) as ChatPostMessageArguments, isDetailResult: true, }); } } else { this._slackRequestQueue.push({ type: SLACK_REQUEST_TYPE.WEBHOOK_SEND, payload: this.createResultPayload( runnerStats, stateCount ) as IncomingWebhookSendArguments, }); } } catch (error) { log.error(error); throw error; } } this._hasRunnerEnd = true; } } export default SlackReporter; export { SlackReporterOptions }; export * from './types.js'; declare global { namespace WebdriverIO { // eslint-disable-next-line @typescript-eslint/no-empty-interface interface ReporterOption extends SlackReporterOptions {} } namespace NodeJS { interface Process { emit( event: typeof EVENTS.POST_MESSAGE, payload: ChatPostMessageArguments ): boolean; emit( event: typeof EVENTS.UPLOAD, payload: FilesUploadArguments ): Promise<WebAPICallResult>; emit( event: typeof EVENTS.SEND, payload: IncomingWebhookSendArguments ): boolean; emit(event: typeof EVENTS.SCREENSHOT, buffer: Buffer): boolean; emit( event: typeof EVENTS.RESULT, args: { result: WebAPICallResult | IncomingWebhookResult | undefined; error: any; } ): boolean; on( event: typeof EVENTS.POST_MESSAGE, listener: ( payload: ChatPostMessageArguments ) => Promise<WebAPICallResult> ): this; on( event: typeof EVENTS.UPLOAD, listener: (payload: FilesUploadArguments) => Promise<WebAPICallResult> ): this; on( event: typeof EVENTS.SEND, listener: ( payload: IncomingWebhookSendArguments ) => Promise<IncomingWebhookResult> ): this; on( event: typeof EVENTS.SCREENSHOT, listener: (buffer: Buffer) => void ): this; once( event: typeof EVENTS.RESULT, listener: (args: { result: WebAPICallResult | IncomingWebhookResult | undefined; error: any; }) => Promise<void> ): this; } } }