playwright-slack-report 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 ryanrosello-og
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # playwright-slack-report ![Biulds](https://github.com/ryanrosello-og/playwright-slack-report/actions/workflows/playwright.yml/badge.svg) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ryanrosello-og/playwright-slack-report/blob/master/LICENSE)
2
+
3
+ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ryanrosello-og/playwright-slack-report)
4
+
5
+ Publish your Playwright test results to your favorite Slack channel(s).
6
+
7
+ ![Gif](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-13_8-35-26.gif?raw=true)
8
+
9
+ ## 🚀 Features
10
+
11
+ - 💌 Send results your Playwright test results to one or more Slack channels
12
+ - 📊 Conditionally send results to Slack channels based on test results
13
+ - 📄 Include additional meta information into your test summary e.g. Branch, BuildId etc
14
+ - 🧑‍🎨 Define your own custom Slack message layout!
15
+
16
+
17
+ # đŸ“Ļ Installation
18
+
19
+ Run following commands:
20
+
21
+ **yarn**
22
+
23
+ `yarn add playwright-slack-report -D`
24
+
25
+ **npm**
26
+
27
+ `npm install playwright-slack-report -D`
28
+
29
+ Modify your `playwright.config.ts` file to include the following:
30
+
31
+ ```typescript
32
+ reporter: [
33
+ [
34
+ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
35
+ {
36
+ channels: ["pw-tests", "ci"], // provide one or more Slack channels
37
+ sendResults: "always", // "always" , "on-failure", "off"
38
+ },
39
+ ],
40
+ ["dot"], // other reporters
41
+ ],
42
+ ```
43
+
44
+ Run your tests by providing your` SLACK_BOT_USER_OAUTH_TOKEN` as an environment variable:
45
+
46
+ `SLACK_BOT_USER_OAUTH_TOKEN=[your Slack bot user OAUTH token] npx playwright test`
47
+
48
+ # â„šī¸ How do I find my Slack bot oauth token?
49
+
50
+ You will need to have Slack administrator rights to perform the steps below.
51
+
52
+ 1. Navigate to https://api.slack.com/apps
53
+ 2. Click the Create New App button and select "From scratch"
54
+
55
+ ![Navigate to https://api.slack.com/apps](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-37-11.png?raw=true)
56
+
57
+ 3. Input a name for your app and select the target workspace, then click on the **Create App** button
58
+
59
+ ![Input a name for your app and select the target workspace](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-40-51.png?raw=true)
60
+
61
+ 4. Under the Features menu, select **OAuth & Permissions** and scroll down to **Scopes** section
62
+
63
+ ![Under the Features menu select](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-44-29.png?raw=true)
64
+
65
+ 5. Click the **Add an OAuth Scope** button and select the following scopes:
66
+
67
+ ![Click the Add an OAuth Scope](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-48-30.png?raw=true)
68
+
69
+ * chat:write
70
+ * chat:write.public
71
+ * chat:write.customize
72
+
73
+ 6. Scroll up to the OAuth Tokens for Your Workspace and click the **Install to Workspace** button
74
+
75
+ ![Install](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-55-22.png?raw=true)
76
+
77
+ > You will be prompted with the message below, click the Allow button
78
+
79
+ ![click the Allow button](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-49-49.png?raw=true)
80
+
81
+ The final step will be to copy the generated Bot User OAuth Token aka `SLACK_BOT_USER_OAUTH_TOKEN`.
82
+
83
+ >**Treat this token as a secret.**
84
+
85
+ ![Final](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-53-17.png?raw=true)
86
+
87
+ # âš™ī¸ Configuration
88
+
89
+ An example advanced configuration is shown below:
90
+
91
+
92
+ ```typescript
93
+ import { generateCustomLayout } from "./my_custom_layout";
94
+
95
+ ...
96
+
97
+ reporter: [
98
+ [
99
+ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
100
+ {
101
+ channels: ["pw-tests", "ci"], // provide one or more Slack channels
102
+ sendResults: "always", // "always" , "on-failure", "off"
103
+ },
104
+ layout: generateCustomLayout,
105
+ meta: [
106
+ {
107
+ key: 'BUILD_NUMBER',
108
+ value: '323332-2341',
109
+ },
110
+ {
111
+ key: 'WHATEVER_ENV_VARIABLE',
112
+ value: process.env.SOME_ENV_VARIABLE, // depending on your CI environment, this can be the branch name, build id, etc
113
+ },
114
+ ],
115
+ ],
116
+ ],
117
+ ```
118
+
119
+ ### **channels**
120
+ An array of Slack channels to post to, atleast one channel is required
121
+ ### **sendResults**
122
+ Can either be *"always"*, *"on-failure"* or *"off"*, this configuration is required:
123
+ * **always** - will send the results to Slack at completion of the test run
124
+ * **on-failure** - will send the results to Slack only if a test failures are encountered
125
+ * **off** - turns off the reporter, it will not send the results to Slack
126
+ ### **layout**
127
+ A function that returns a layout object, this configuration is optional. See section below for more details.
128
+ * meta - an array of meta data to be sent to Slack, this configuration is optional.
129
+
130
+ **Examples:**
131
+ ```typescript
132
+ ...
133
+ meta: [
134
+ {
135
+ key: 'Suite',
136
+ value: 'Nightly full regression',
137
+ },
138
+ {
139
+ key: 'GITHUB_REPOSITORY',
140
+ value: 'octocat/telsa-ui',
141
+ },
142
+ {
143
+ key: 'GITHUB_REF',
144
+ value: process.env.GITHUB_REF,
145
+ },
146
+ ],
147
+ ...
148
+ ```
149
+
150
+ # 🎨 Define your own Slack message custom layout
151
+
152
+ You can define your own Slack message layout to suit your needs.
153
+
154
+ Firstly, install the necessary type definitions:
155
+
156
+ `yarn add @slack/types -D`
157
+
158
+
159
+ Next, define your layout function. The signature of this function should adhere to example below:
160
+
161
+ ```typescript
162
+ import { Block, KnownBlock } from "@slack/types";
163
+ import { SummaryResults } from "playwright-slack-report/dist/src";
164
+
165
+ const generateCustomLayout = (summaryResults: SummaryResults):Array<KnownBlock | Block> => {
166
+ // your implementation goes here
167
+ }
168
+
169
+ export default generateCustomLayout;
170
+ ```
171
+
172
+ In your, `playwright.confing.ts` file, add your function into the config.
173
+
174
+ ```typescript
175
+ import { generateCustomLayout } from "./my_custom_layout";
176
+
177
+ ...
178
+
179
+ reporter: [
180
+ [
181
+ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
182
+ {
183
+ channels: ["pw-tests", "ci"], // provide one or more Slack channels
184
+ sendResults: "always", // "always" , "on-failure", "off"
185
+ },
186
+ layout: generateCustomLayout,
187
+ ...
188
+ ],
189
+ ],
190
+ ```
191
+
192
+ >Pro Tip: You can use the [block-kit provided by Slack when creating your layout.](https://app.slack.com/block-kit-builder/)
193
+
194
+ ### Examples:
195
+
196
+ **Example 1: - very simple summary**
197
+
198
+ ```typescript
199
+ import { Block, KnownBlock } from '@slack/types';
200
+ import { SummaryResults } from '..';
201
+
202
+ export default function generateCustomLayoutSimpleExample(
203
+ summaryResults: SummaryResults,
204
+ ): Array<Block | KnownBlock> {
205
+ return [
206
+ {
207
+ type: 'section',
208
+ text: {
209
+ type: 'mrkdwn',
210
+ text:
211
+ summaryResults.failed === 0
212
+ ? ':tada: All tests passed!'
213
+ : `😭${summaryResults.failed} failure(s) out of ${summaryResults.tests.length} tests`,
214
+ },
215
+ },
216
+ ];
217
+ }
218
+ ```
219
+
220
+ Generates the following message in Slack:
221
+
222
+ ![Final](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-13_8-02-54.png?raw=true)
223
+
224
+
225
+ **Example 2: - very simple summary (with Meta information)**
226
+
227
+ Add the meta block in your config:
228
+
229
+ ```typescript
230
+ reporter: [
231
+ [
232
+ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
233
+ {
234
+ channels: ["demo"],
235
+ sendResults: "always", // "always" , "on-failure", "off",
236
+ layout: generateCustomLayout,
237
+ meta: [
238
+ {
239
+ key: 'EXAMPLE_META_node_env',
240
+ value: process.env.HOME ,
241
+ },
242
+ ],
243
+ },
244
+
245
+ ],
246
+ ],
247
+ ```
248
+
249
+ Create the function to generate the layout:
250
+
251
+ ```typescript
252
+ import { Block, KnownBlock } from '@slack/types';
253
+ import { SummaryResults } from '..';
254
+
255
+ export default function generateCustomLayoutSimpleMeta(
256
+ summaryResults: SummaryResults,
257
+ ): Array<Block | KnownBlock> {
258
+ const meta: { type: string; text: { type: string; text: string; }; }[] = [];
259
+ if (summaryResults.meta) {
260
+ for (let i = 0; i < summaryResults.meta.length; i += 1) {
261
+ const { key, value } = summaryResults.meta[i];
262
+ meta.push({
263
+ type: 'section',
264
+ text: {
265
+ type: 'mrkdwn',
266
+ text: `\n*${key}* :\t${value}`,
267
+ },
268
+ });
269
+ }
270
+ }
271
+ return [
272
+ {
273
+ type: 'section',
274
+ text: {
275
+ type: 'mrkdwn',
276
+ text:
277
+ summaryResults.failed === 0
278
+ ? ':tada: All tests passed!'
279
+ : `😭${summaryResults.failed} failure(s) out of ${summaryResults.tests.length} tests`,
280
+ },
281
+ },
282
+ ...meta,
283
+ ];
284
+ }
285
+
286
+ ```
287
+
288
+ Generates the following message in Slack:
289
+
290
+ ![Final](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-13_8-17-46.png?raw=true)
291
+
292
+ # 🔑 License
293
+
294
+ [MIT](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/LICENSE)
295
+
296
+ # ✨ Contributing
297
+
298
+ Clone the project and run `npm install`
299
+
300
+ Make your changes
301
+ Run the tests using `npm run pw`
302
+
303
+ **To execute and test the entire package:**
304
+
305
+ Run `npm pack`
306
+
307
+ Create a new playwright project using `yarn create playwright`
308
+ Modify the `package.json` and a local dependancy to the generated `tgz` file
309
+
310
+ e.g.
311
+
312
+ ```
313
+ "dependencies": {
314
+ "playwright-slack-report": "/home/ry/_repo/playwright-slack-report/playwright-slack-report-1.0.3.tgz"
315
+ }
316
+ ```
317
+
318
+ * Execute `npm install`
319
+ * Set your `SLACK_BOT_USER_OAUTH_TOKEN` environment variable
320
+ * Modify the `playwright.config.ts` as above
321
+ * Run the tests using `npx playwright text`
322
+
323
+ # 🐛 Something not working for you?
324
+
325
+ Feel free to [raise a github issue](https://github.com/ryanrosello-og/playwright-slack-report/issues) for any bugs or feature requests.
@@ -0,0 +1,4 @@
1
+ import { KnownBlock, Block } from '@slack/types';
2
+ import { SummaryResults } from '.';
3
+ declare const generateBlocks: (summaryResults: SummaryResults) => Promise<Array<KnownBlock | Block>>;
4
+ export default generateBlocks;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const generateBlocks = async (summaryResults) => {
4
+ const maxNumberOfFailures = 10;
5
+ const maxNumberOfFailureLength = 650;
6
+ const fails = [];
7
+ const meta = [];
8
+ const summary = {
9
+ type: 'section',
10
+ text: {
11
+ type: 'mrkdwn',
12
+ text: `:white_check_mark: *${summaryResults.passed}* Tests ran successfully \n\n :red_circle: *${summaryResults.failed}* Tests failed \n\n ${summaryResults.skipped > 0
13
+ ? `:fast_forward: *${summaryResults.skipped}* skipped`
14
+ : ''} \n\n ${summaryResults.aborted > 0
15
+ ? `:exclamation: *${summaryResults.aborted}* aborted`
16
+ : ''}`,
17
+ },
18
+ };
19
+ for (let i = 0; i < summaryResults.failures.length; i += 1) {
20
+ const { failureReason, test } = summaryResults.failures[i];
21
+ const formattedFailure = failureReason
22
+ .substring(0, maxNumberOfFailureLength)
23
+ .split('\n')
24
+ .map((l) => `>${l}`)
25
+ .join('\n');
26
+ fails.push({
27
+ type: 'section',
28
+ text: {
29
+ type: 'mrkdwn',
30
+ text: `*${test}*
31
+ \n\n${formattedFailure}`,
32
+ },
33
+ });
34
+ if (i > maxNumberOfFailures) {
35
+ fails.push({
36
+ type: 'section',
37
+ text: {
38
+ type: 'mrkdwn',
39
+ text: '*There are too many failures to display*',
40
+ },
41
+ });
42
+ break;
43
+ }
44
+ }
45
+ if (summaryResults.meta) {
46
+ for (let i = 0; i < summaryResults.meta.length; i += 1) {
47
+ const { key, value } = summaryResults.meta[i];
48
+ meta.push({
49
+ type: 'section',
50
+ text: {
51
+ type: 'mrkdwn',
52
+ text: `\n*${key}* :\t${value}`,
53
+ },
54
+ });
55
+ }
56
+ }
57
+ return [
58
+ summary,
59
+ ...meta,
60
+ {
61
+ type: 'divider',
62
+ },
63
+ ...fails,
64
+ ];
65
+ };
66
+ exports.default = generateBlocks;
@@ -0,0 +1,37 @@
1
+ /// <reference types="node" />
2
+ import { failure, SummaryResults } from '.';
3
+ export declare type testResult = {
4
+ suiteName: string;
5
+ name: string;
6
+ browser?: string;
7
+ endedAt: string;
8
+ reason: string;
9
+ retry: number;
10
+ startedAt: string;
11
+ status: 'failed' | 'passed' | 'skipped' | 'aborted';
12
+ attachments?: {
13
+ body: string | undefined | Buffer;
14
+ contentType: string;
15
+ name: string;
16
+ path: string;
17
+ }[];
18
+ };
19
+ export declare type testSuite = {
20
+ testSuite: {
21
+ title: string;
22
+ tests: testResult[];
23
+ testRunId?: number;
24
+ };
25
+ };
26
+ export default class ResultsParser {
27
+ private result;
28
+ constructor();
29
+ getParsedResults(): Promise<SummaryResults>;
30
+ getFailures(): Promise<Array<failure>>;
31
+ updateResults(data: {
32
+ testSuite: any;
33
+ }): void;
34
+ addTestResult(suiteName: any, test: any): void;
35
+ parseTests(suiteName: any, tests: any): Promise<testResult[]>;
36
+ cleanseReason(rawReaseon: string): string;
37
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /* eslint-disable import/extensions */
3
+ /* eslint-disable no-control-regex */
4
+ /* eslint-disable class-methods-use-this */
5
+ /* eslint-disable no-param-reassign */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ class ResultsParser {
8
+ result;
9
+ constructor() {
10
+ this.result = [];
11
+ }
12
+ async getParsedResults() {
13
+ const summary = {
14
+ passed: 0,
15
+ failed: 0,
16
+ skipped: 0,
17
+ aborted: 0,
18
+ failures: await this.getFailures(),
19
+ tests: [],
20
+ };
21
+ for (const suite of this.result) {
22
+ summary.tests = summary.tests.concat(suite.testSuite.tests);
23
+ for (const test of suite.testSuite.tests) {
24
+ if (test.status === 'passed') {
25
+ summary.passed += 1;
26
+ }
27
+ else if (test.status === 'failed') {
28
+ summary.failed += 1;
29
+ }
30
+ else if (test.status === 'skipped') {
31
+ summary.skipped += 1;
32
+ }
33
+ else if (test.status === 'aborted') {
34
+ summary.aborted += 1;
35
+ }
36
+ }
37
+ }
38
+ return summary;
39
+ }
40
+ async getFailures() {
41
+ const failures = [];
42
+ for (const suite of this.result) {
43
+ for (const test of suite.testSuite.tests) {
44
+ if (test.status === 'failed') {
45
+ failures.push({
46
+ test: test.name,
47
+ failureReason: test.reason,
48
+ });
49
+ }
50
+ }
51
+ }
52
+ return failures;
53
+ }
54
+ updateResults(data) {
55
+ if (data.testSuite.tests.length > 0) {
56
+ this.result.push(data);
57
+ }
58
+ }
59
+ addTestResult(suiteName, test) {
60
+ const testResults = [];
61
+ for (const result of test.results) {
62
+ testResults.push({
63
+ suiteName,
64
+ name: test.title,
65
+ status: result.status,
66
+ retry: result.retry,
67
+ startedAt: new Date(result.startTime).toISOString(),
68
+ endedAt: new Date(new Date(result.startTime).getTime() + result.duration).toISOString(),
69
+ reason: `${this.cleanseReason(result.error?.message)} \n ${this.cleanseReason(result.error?.stack)}`,
70
+ attachments: result.attachments,
71
+ });
72
+ }
73
+ this.updateResults({
74
+ testSuite: {
75
+ title: suiteName,
76
+ tests: testResults,
77
+ },
78
+ });
79
+ }
80
+ async parseTests(suiteName, tests) {
81
+ const testResults = [];
82
+ for (const test of tests) {
83
+ for (const result of test.results) {
84
+ testResults.push({
85
+ suiteName,
86
+ name: test.title,
87
+ status: result.status,
88
+ retry: result.retry,
89
+ startedAt: new Date(result.startTime).toISOString(),
90
+ endedAt: new Date(new Date(result.startTime).getTime() + result.duration).toISOString(),
91
+ reason: `${this.cleanseReason(result.error?.message)} \n ${this.cleanseReason(result.error?.stack)}`,
92
+ attachments: result.attachments,
93
+ });
94
+ }
95
+ }
96
+ return testResults;
97
+ }
98
+ cleanseReason(rawReaseon) {
99
+ // eslint-disable-next-line prefer-regex-literals
100
+ const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
101
+ return rawReaseon ? rawReaseon.replace(ansiRegex, '') : '';
102
+ }
103
+ }
104
+ exports.default = ResultsParser;
@@ -0,0 +1,22 @@
1
+ import { WebClient, KnownBlock, Block, ChatPostMessageResponse } from '@slack/web-api';
2
+ import { SummaryResults } from '.';
3
+ export declare type additionalInfo = Array<{
4
+ key: string;
5
+ value: string;
6
+ }>;
7
+ export default class SlackClient {
8
+ private slackWebClient;
9
+ constructor(slackClient: WebClient);
10
+ sendMessage({ options, }: {
11
+ options: {
12
+ channelIds: Array<string>;
13
+ summaryResults: SummaryResults;
14
+ customLayout: Function | undefined;
15
+ fakeRequest?: Function;
16
+ };
17
+ }): Promise<Array<{
18
+ channel: string;
19
+ outcome: string;
20
+ }>>;
21
+ doPostRequest(channel: string, blocks: Array<KnownBlock | Block>): Promise<ChatPostMessageResponse>;
22
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const LayoutGenerator_1 = require("./LayoutGenerator");
4
+ class SlackClient {
5
+ slackWebClient;
6
+ constructor(slackClient) {
7
+ this.slackWebClient = slackClient;
8
+ }
9
+ async sendMessage({ options, }) {
10
+ let blocks;
11
+ if (options.customLayout) {
12
+ blocks = options.customLayout(options.summaryResults);
13
+ }
14
+ else {
15
+ blocks = await (0, LayoutGenerator_1.default)(options.summaryResults);
16
+ }
17
+ if (!options.channelIds) {
18
+ throw new Error(`Channel ids [${options.channelIds}] is not valid`);
19
+ }
20
+ const result = [];
21
+ for (const channel of options.channelIds) {
22
+ let chatResponse;
23
+ try {
24
+ // under test
25
+ if (options.fakeRequest) {
26
+ chatResponse = await options.fakeRequest();
27
+ }
28
+ else {
29
+ // send request for reals
30
+ chatResponse = await this.doPostRequest(channel, blocks);
31
+ }
32
+ if (chatResponse.ok) {
33
+ result.push({ channel, outcome: `✅ Message sent to ${channel}` });
34
+ // eslint-disable-next-line no-console
35
+ console.log(`✅ Message sent to ${channel}`);
36
+ }
37
+ }
38
+ catch (error) {
39
+ result.push({
40
+ channel,
41
+ outcome: `❌ Message not sent to ${channel} \r\n ${error.message}`,
42
+ });
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ async doPostRequest(channel, blocks) {
48
+ const chatResponse = await this.slackWebClient.chat.postMessage({
49
+ channel,
50
+ text: ' ',
51
+ blocks,
52
+ });
53
+ return chatResponse;
54
+ }
55
+ }
56
+ exports.default = SlackClient;
@@ -0,0 +1,19 @@
1
+ import { FullConfig, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
2
+ declare class SlackReporter implements Reporter {
3
+ private suite;
4
+ private sendResults;
5
+ private slackChannels;
6
+ private meta;
7
+ private customLayout;
8
+ private resultsParser;
9
+ logs: string[];
10
+ onBegin(fullConfig: FullConfig, suite: Suite): void;
11
+ onTestEnd(test: TestCase, result: TestResult): void;
12
+ onEnd(): Promise<void>;
13
+ preChecks(): {
14
+ okToProceed: boolean;
15
+ message?: string;
16
+ };
17
+ log(message: string | undefined): void;
18
+ }
19
+ export default SlackReporter;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const web_api_1 = require("@slack/web-api");
4
+ const ResultsParser_1 = require("./ResultsParser");
5
+ const SlackClient_1 = require("./SlackClient");
6
+ class SlackReporter {
7
+ suite;
8
+ sendResults = 'on-failure';
9
+ slackChannels = [];
10
+ meta = [];
11
+ customLayout;
12
+ resultsParser;
13
+ logs = [];
14
+ onBegin(fullConfig, suite) {
15
+ this.suite = suite;
16
+ this.logs = [];
17
+ const slackReporterConfig = fullConfig.reporter.filter((f) => f[0].toLowerCase().includes('slackreporter'))[0][1];
18
+ if (slackReporterConfig) {
19
+ this.meta = slackReporterConfig.meta || [];
20
+ this.sendResults = slackReporterConfig.sendResults || 'always';
21
+ this.customLayout = slackReporterConfig.layout;
22
+ this.slackChannels = slackReporterConfig.channels;
23
+ }
24
+ this.resultsParser = new ResultsParser_1.default();
25
+ }
26
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
27
+ onTestEnd(test, result) {
28
+ this.resultsParser.addTestResult(test.parent.title, test);
29
+ }
30
+ async onEnd() {
31
+ const { okToProceed, message } = this.preChecks();
32
+ if (!okToProceed) {
33
+ this.log(message);
34
+ return;
35
+ }
36
+ const resultSummary = await this.resultsParser.getParsedResults();
37
+ resultSummary.meta = this.meta;
38
+ if (this.sendResults === 'on-failure'
39
+ && resultSummary.failures.length === 0) {
40
+ this.log('⏊ Slack reporter - no failures found');
41
+ return;
42
+ }
43
+ const slackClient = new SlackClient_1.default(new web_api_1.WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN, {
44
+ logLevel: web_api_1.LogLevel.DEBUG,
45
+ }));
46
+ await slackClient.sendMessage({
47
+ options: {
48
+ channelIds: this.slackChannels,
49
+ summaryResults: resultSummary,
50
+ customLayout: this.customLayout,
51
+ },
52
+ });
53
+ }
54
+ preChecks() {
55
+ if (this.sendResults === 'off') {
56
+ return { okToProceed: false, message: '❌ Slack reporter is disabled' };
57
+ }
58
+ if (!process.env.SLACK_BOT_USER_OAUTH_TOKEN) {
59
+ return {
60
+ okToProceed: false,
61
+ message: '❌ SLACK_BOT_USER_OAUTH_TOKEN was not found',
62
+ };
63
+ }
64
+ if (!this.sendResults
65
+ || !['always', 'on-failure', 'off'].includes(this.sendResults)) {
66
+ return {
67
+ okToProceed: false,
68
+ message: "❌ \"sendResults\" is not valid. Expecting one of ['always', 'on-failure', 'off'].",
69
+ };
70
+ }
71
+ if (!this.sendResults || this.slackChannels?.length === 0) {
72
+ return {
73
+ okToProceed: false,
74
+ message: '❌ Slack channel(s) was not provided in the config',
75
+ };
76
+ }
77
+ if (this.customLayout && typeof this.customLayout !== 'function') {
78
+ return {
79
+ okToProceed: false,
80
+ message: '❌ Custom layout is not a function',
81
+ };
82
+ }
83
+ if (this.meta && !Array.isArray(this.meta)) {
84
+ return { okToProceed: false, message: '❌ Meta is not an array' };
85
+ }
86
+ return { okToProceed: true };
87
+ }
88
+ log(message) {
89
+ // eslint-disable-next-line no-console
90
+ console.log(message);
91
+ if (message) {
92
+ this.logs.push(message);
93
+ }
94
+ }
95
+ }
96
+ exports.default = SlackReporter;
@@ -0,0 +1,4 @@
1
+ import { Block, KnownBlock } from '@slack/types';
2
+ import { SummaryResults } from '..';
3
+ declare const generateCustomLayout: (summaryResults: SummaryResults) => Array<KnownBlock | Block>;
4
+ export default generateCustomLayout;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const generateCustomLayout = (summaryResults) => {
4
+ const maxNumberOfFailures = 10;
5
+ const maxNumberOfFailureLength = 650;
6
+ const fails = [];
7
+ const meta = [];
8
+ for (let i = 0; i < summaryResults.failures.length; i += 1) {
9
+ const { failureReason, test } = summaryResults.failures[i];
10
+ const formattedFailure = failureReason
11
+ .substring(0, maxNumberOfFailureLength)
12
+ .split('\n')
13
+ .map((l) => `>${l}`)
14
+ .join('\n');
15
+ fails.push({
16
+ type: 'section',
17
+ text: {
18
+ type: 'mrkdwn',
19
+ text: `*${test}*
20
+ \n\n${formattedFailure}`,
21
+ },
22
+ });
23
+ if (i > maxNumberOfFailures) {
24
+ fails.push({
25
+ type: 'section',
26
+ text: {
27
+ type: 'mrkdwn',
28
+ text: '*There are too many failures to display, view the full results in BuildKite*',
29
+ },
30
+ });
31
+ break;
32
+ }
33
+ }
34
+ if (summaryResults.meta) {
35
+ for (let i = 0; i < summaryResults.meta.length; i += 1) {
36
+ const { key, value } = summaryResults.meta[i];
37
+ meta.push({
38
+ type: 'section',
39
+ text: {
40
+ type: 'mrkdwn',
41
+ text: `\n*${key}* :\t${value}`,
42
+ },
43
+ });
44
+ }
45
+ }
46
+ const testWithStringAttachment = summaryResults.tests.filter((t) => t.attachments)[0];
47
+ return [
48
+ {
49
+ type: 'section',
50
+ text: {
51
+ type: 'mrkdwn',
52
+ text: '**Thisi is cusomter block**',
53
+ },
54
+ },
55
+ {
56
+ type: 'section',
57
+ text: {
58
+ type: 'mrkdwn',
59
+ text: `**The first test is:**\n${summaryResults.tests[0].name}`,
60
+ },
61
+ },
62
+ {
63
+ type: 'section',
64
+ text: {
65
+ type: 'mrkdwn',
66
+ text: `**Test with attachment:**\n${testWithStringAttachment?.attachments
67
+ ?.filter((a) => a.contentType === 'text/plain')
68
+ .map((a) => a.body?.toString())}`,
69
+ },
70
+ },
71
+ ...meta,
72
+ {
73
+ type: 'section',
74
+ text: {
75
+ type: 'mrkdwn',
76
+ text: `:white_check_mark: *${summaryResults.passed}* Tests ran successfully \n\n :red_circle: *${summaryResults.failed}* Tests failed \n\n ${summaryResults.skipped > 0
77
+ ? `:fast_forward: *${summaryResults.skipped}* skipped`
78
+ : ''} \n\n ${summaryResults.aborted > 0
79
+ ? `:exclamation: *${summaryResults.aborted}* aborted`
80
+ : ''}`,
81
+ },
82
+ },
83
+ {
84
+ type: 'divider',
85
+ },
86
+ ...fails,
87
+ ];
88
+ };
89
+ exports.default = generateCustomLayout;
@@ -0,0 +1,3 @@
1
+ import { Block, KnownBlock } from '@slack/types';
2
+ import { SummaryResults } from '..';
3
+ export default function generateCustomLayoutSimpleExample(summaryResults: SummaryResults): Array<Block | KnownBlock>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function generateCustomLayoutSimpleExample(summaryResults) {
4
+ return [
5
+ {
6
+ type: 'section',
7
+ text: {
8
+ type: 'mrkdwn',
9
+ text: summaryResults.failed === 0
10
+ ? ':tada: All tests passed!'
11
+ : `😭${summaryResults.failed} failure(s) out of ${summaryResults.tests.length} tests`,
12
+ },
13
+ },
14
+ ];
15
+ }
16
+ exports.default = generateCustomLayoutSimpleExample;
@@ -0,0 +1,3 @@
1
+ import { Block, KnownBlock } from '@slack/types';
2
+ import { SummaryResults } from '..';
3
+ export default function generateCustomLayoutSimpleMeta(summaryResults: SummaryResults): Array<Block | KnownBlock>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function generateCustomLayoutSimpleMeta(summaryResults) {
4
+ const meta = [];
5
+ if (summaryResults.meta) {
6
+ for (let i = 0; i < summaryResults.meta.length; i += 1) {
7
+ const { key, value } = summaryResults.meta[i];
8
+ meta.push({
9
+ type: 'section',
10
+ text: {
11
+ type: 'mrkdwn',
12
+ text: `\n*${key}* :\t${value}`,
13
+ },
14
+ });
15
+ }
16
+ }
17
+ return [
18
+ {
19
+ type: 'section',
20
+ text: {
21
+ type: 'mrkdwn',
22
+ text: summaryResults.failed === 0
23
+ ? ':tada: All tests passed!'
24
+ : `😭${summaryResults.failed} failure(s) out of ${summaryResults.tests.length} tests`,
25
+ },
26
+ },
27
+ ...meta,
28
+ ];
29
+ }
30
+ exports.default = generateCustomLayoutSimpleMeta;
@@ -0,0 +1,32 @@
1
+ /// <reference types="node" />
2
+ export declare type SummaryResults = {
3
+ passed: number;
4
+ failed: number;
5
+ skipped: number;
6
+ aborted: number;
7
+ failures: Array<failure>;
8
+ meta?: Array<{
9
+ key: string;
10
+ value: string;
11
+ }>;
12
+ tests: Array<{
13
+ suiteName: string;
14
+ name: string;
15
+ browser?: string;
16
+ endedAt: string;
17
+ reason: string;
18
+ retry: number;
19
+ startedAt: string;
20
+ status: 'failed' | 'passed' | 'skipped' | 'aborted';
21
+ attachments?: {
22
+ body: string | undefined | Buffer;
23
+ contentType: string;
24
+ name: string;
25
+ path: string;
26
+ }[];
27
+ }>;
28
+ };
29
+ export declare type failure = {
30
+ test: string;
31
+ failureReason: string;
32
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "dependencies": {
3
+ "@playwright/test": "^1.23.3",
4
+ "@slack/web-api": "^6.7.2",
5
+ "dotenv": "^16.0.1",
6
+ "playwright": "^1.23.3",
7
+ "typescript": "^4.7.4",
8
+ "@slack/types": "^2.7.0"
9
+ },
10
+ "devDependencies": {
11
+ "@typescript-eslint/eslint-plugin": "^5.30.6",
12
+ "@typescript-eslint/parser": "^5.30.6",
13
+ "eslint": "^7.32.0 || ^8.2.0",
14
+ "eslint-config-airbnb-base": "^15.0.0",
15
+ "eslint-config-node": "^4.1.0",
16
+ "eslint-config-prettier": "^8.5.0",
17
+ "eslint-plugin-import": "^2.25.2",
18
+ "eslint-plugin-node": "^11.1.0",
19
+ "eslint-plugin-prettier": "^4.2.1",
20
+ "nyc": "^15.1.0",
21
+ "prettier": "^2.7.1",
22
+ "ts-mockito": "^2.6.1"
23
+ },
24
+ "scripts": {
25
+ "prettier": "prettier --write --loglevel warn \"**/**/*.ts\"",
26
+ "pw": "nyc playwright test",
27
+ "build": "tsc -p ./tsconfig.json",
28
+ "lint":"npx eslint . --ext .ts"
29
+ },
30
+ "name": "playwright-slack-report",
31
+ "version": "1.0.4",
32
+ "main": "index.js",
33
+ "types": "dist/index.d.ts",
34
+ "repository": "git@github.com:ryanrosello-og/playwright-slack-report.git",
35
+ "author": "Ryan Rosello <ryanrosello@hotmail.com>",
36
+ "license": "MIT",
37
+ "files": ["/dist/src"]
38
+ }