Skip to content

Commit

Permalink
Add option to send message via webhooks (#50)
Browse files Browse the repository at this point in the history
* fix line endings

* WIP: send via slack webhook

* send via webhooks - unit test

* update doco

* fix lint

* fix doco

* version bump

* add '
  • Loading branch information
ryanrosello-og authored Aug 24, 2023
1 parent 841e88f commit a6fe210
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 41 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
],
"no-empty-pattern": 0,
"no-undef": 0,
"no-use-before-define": 0
"no-use-before-define": 0,
"operator-linebreak": ["error", "before"]
}
}

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,35 @@ Modify your `playwright.config.ts` file to include the following:
["dot"], // other reporters
],
```
# Option A - send your results via a Slack webhook

Enable incoming webhooks in your Slack workspace by following the steps as per Slack's documentation:

https://api.slack.com/messaging/webhooks



Once you have enabled incoming webhooks, you will need to copy the webhook URL and specify it in the config:

```typescript
reporter: [
[
"./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
{
slackWebHookUrl: "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
sendResults: "always", // "always" , "on-failure", "off"
},
],
["dot"], // other reporters
],
```
### Note I:
You will most likely need to have Slack administrator rights to perform the steps above.

### Note II:
Sending failure details in a thread is not supported when using webhooks. You will need to use Option B below.

# Option B
Run your tests by providing your `SLACK_BOT_USER_OAUTH_TOKEN` as an environment variable or specifying `slackOAuthToken` option in the config:

`SLACK_BOT_USER_OAUTH_TOKEN=[your Slack bot user OAUTH token] npx playwright test`
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"dependencies": {
"@slack/web-api": "^6.8.1",
"@slack/webhook": "^6.1.0",
"https-proxy-agent": "^7.0.1"
},
"devDependencies": {
Expand All @@ -18,7 +19,7 @@
"eslint-plugin-prettier": "^4.2.1",
"nyc": "^15.1.0",
"prettier": "^2.7.1",
"ts-mockito": "^2.6.1",
"ts-sinon": "^2.0.2",
"typescript": "^4.7.4"
},
"scripts": {
Expand All @@ -28,7 +29,7 @@
"lint": "npx eslint . --ext .ts"
},
"name": "playwright-slack-report",
"version": "1.1.21",
"version": "1.1.22",
"main": "index.js",
"types": "dist/index.d.ts",
"repository": "[email protected]:ryanrosello-og/playwright-slack-report.git",
Expand Down
5 changes: 4 additions & 1 deletion src/ResultsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ export default class ResultsParser {

addTestResult(suiteName: any, testCase: any, projectBrowserMapping: any) {
const testResults: testResult[] = [];
const projectSettings = this.determineBrowser(testCase._projectId, projectBrowserMapping);
const projectSettings = this.determineBrowser(
testCase._projectId,
projectBrowserMapping,
);
for (const result of testCase.results) {
testResults.push({
suiteName,
Expand Down
98 changes: 69 additions & 29 deletions src/SlackReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
} from '@playwright/test/reporter';
import { LogLevel, WebClient } from '@slack/web-api';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { IncomingWebhook } from '@slack/webhook';
import ResultsParser from './ResultsParser';
import SlackClient from './SlackClient';
import SlackWebhookClient from './SlackWebhookClient';

class SlackReporter implements Reporter {
private customLayout: Function | undefined;
Expand All @@ -33,11 +35,13 @@ class SlackReporter implements Reporter {

private slackOAuthToken: string | undefined;

private slackWebHookUrl: string | undefined;

private disableUnfurl: boolean | undefined;

private proxy: string | undefined;

private browsers: {projectName: string ; browser: string}[] = [];
private browsers: { projectName: string; browser: string }[] = [];

private suite!: Suite;

Expand All @@ -51,7 +55,12 @@ class SlackReporter implements Reporter {
this.browsers = [];
} else {
// eslint-disable-next-line max-len
this.browsers = fullConfig.projects.map((obj) => ({ projectName: obj.name, browser: obj.use.browserName ? obj.use.browserName : obj.use.defaultBrowserType }));
this.browsers = fullConfig.projects.map((obj) => ({
projectName: obj.name,
browser: obj.use.browserName
? obj.use.browserName
: obj.use.defaultBrowserType,
}));
}

if (slackReporterConfig) {
Expand All @@ -60,8 +69,10 @@ class SlackReporter implements Reporter {
this.customLayout = slackReporterConfig.layout;
this.customLayoutAsync = slackReporterConfig.layoutAsync;
this.slackChannels = slackReporterConfig.channels;
this.maxNumberOfFailuresToShow = slackReporterConfig.maxNumberOfFailuresToShow || 10;
this.maxNumberOfFailuresToShow
= slackReporterConfig.maxNumberOfFailuresToShow || 10;
this.slackOAuthToken = slackReporterConfig.slackOAuthToken || undefined;
this.slackWebHookUrl = slackReporterConfig.slackWebHookUrl || undefined;
this.disableUnfurl = slackReporterConfig.disableUnfurl || false;
this.showInThread = slackReporterConfig.showInThread || false;
this.slackLogLevel = slackReporterConfig.slackLogLevel || LogLevel.DEBUG;
Expand Down Expand Up @@ -98,36 +109,50 @@ class SlackReporter implements Reporter {

const agent = this.proxy ? new HttpsProxyAgent(this.proxy) : undefined;

const slackClient = new SlackClient(
new WebClient(
this.slackOAuthToken || process.env.SLACK_BOT_USER_OAUTH_TOKEN,
{
logLevel: this.slackLogLevel || LogLevel.DEBUG,
agent,
},
),
);

const result = await slackClient.sendMessage({
options: {
channelIds: this.slackChannels,
if (this.slackWebHookUrl) {
const webhook = new IncomingWebhook(this.slackWebHookUrl, { agent });
const slackWebhookClient = new SlackWebhookClient(webhook);
const webhookResult = await slackWebhookClient.sendMessage({
customLayout: this.customLayout,
customLayoutAsync: this.customLayoutAsync,
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
disableUnfurl: this.disableUnfurl,
summaryResults: resultSummary,
showInThread: this.showInThread,
},
});
// eslint-disable-next-line no-console
console.log(JSON.stringify(result, null, 2));
if (this.showInThread && resultSummary.failures.length > 0) {
await slackClient.attachDetailsToThread({
channelIds: this.slackChannels,
ts: result[0].ts,
summaryResults: resultSummary,
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
});
// eslint-disable-next-line no-console
console.log(JSON.stringify(webhookResult, null, 2));
} else {
const slackClient = new SlackClient(
new WebClient(
this.slackOAuthToken || process.env.SLACK_BOT_USER_OAUTH_TOKEN,
{
logLevel: this.slackLogLevel || LogLevel.DEBUG,
agent,
},
),
);

const result = await slackClient.sendMessage({
options: {
channelIds: this.slackChannels,
customLayout: this.customLayout,
customLayoutAsync: this.customLayoutAsync,
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
disableUnfurl: this.disableUnfurl,
summaryResults: resultSummary,
showInThread: this.showInThread,
},
});
// eslint-disable-next-line no-console
console.log(JSON.stringify(result, null, 2));
if (this.showInThread && resultSummary.failures.length > 0) {
await slackClient.attachDetailsToThread({
channelIds: this.slackChannels,
ts: result[0].ts,
summaryResults: resultSummary,
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
});
}
}
}

Expand All @@ -136,11 +161,26 @@ class SlackReporter implements Reporter {
return { okToProceed: false, message: '❌ Slack reporter is disabled' };
}

if (!this.slackOAuthToken && !process.env.SLACK_BOT_USER_OAUTH_TOKEN) {
if (
!this.slackWebHookUrl
&& !this.slackOAuthToken
&& !process.env.SLACK_BOT_USER_OAUTH_TOKEN
) {
return {
okToProceed: false,
message:
'❌ Neither slack webhook url, slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
};
}

if (
this.slackWebHookUrl
&& (process.env.SLACK_BOT_USER_OAUTH_TOKEN || this.slackOAuthToken)
) {
return {
okToProceed: false,
message:
'❌ Neither slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
'❌ You can only enable a single option, either provide a slack webhook url, slackOAuthToken or process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
};
}

Expand Down
54 changes: 54 additions & 0 deletions src/SlackWebhookClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Block, KnownBlock } from '@slack/types';
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
import { SummaryResults } from '.';
import { generateBlocks } from './LayoutGenerator';

export default class SlackWebhookClient {
private webhook: IncomingWebhook;

constructor(webhook: IncomingWebhook) {
this.webhook = webhook;
}

async sendMessage({
customLayout,
customLayoutAsync,
maxNumberOfFailures,
summaryResults,
disableUnfurl,
}: {
customLayout: Function | undefined;
customLayoutAsync: Function | undefined;
maxNumberOfFailures: number;
summaryResults: SummaryResults;
disableUnfurl: boolean;
}): Promise<{ outcome: string }> {
let blocks: (Block | KnownBlock)[];
if (customLayout) {
blocks = customLayout(summaryResults);
} else if (customLayoutAsync) {
blocks = await customLayoutAsync(summaryResults);
} else {
blocks = await generateBlocks(summaryResults, maxNumberOfFailures);
}
let result: IncomingWebhookResult;
try {
result = await this.webhook.send({
blocks,
unfurl_links: !disableUnfurl,
});
} catch (error) {
return {
outcome: `error: ${JSON.stringify(error, null, 2)}`,
};
}
if (result && result.text === 'ok') {
return {
outcome: result.text,
};
}
return {
outcome: '😵 Failed to send webhook message, ensure your webhook url is valid',
};
}
}
4 changes: 2 additions & 2 deletions tests/SlackReporter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ test.describe('SlackReporter - onEnd()', () => {
await fakeSlackReporter.onEnd();
expect(
fakeSlackReporter.logs.includes(
'❌ Neither slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
'❌ Neither slack webhook url, slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
),
).toBeTruthy();
});
Expand Down Expand Up @@ -193,7 +193,7 @@ test.describe('SlackReporter - preChecks()', () => {
expect(result).toEqual({
okToProceed: false,
message:
'❌ Neither slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
'❌ Neither slack webhook url, slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
});
});

Expand Down
Loading

0 comments on commit a6fe210

Please sign in to comment.