@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
text/typescript
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;
}
}
}