playwright-slack-report 1.0.19 → 1.1.0

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/README.md CHANGED
@@ -16,7 +16,7 @@ Publish your Playwright test results to your favorite Slack channel(s).
16
16
  - 🧑‍🎨 Define your own custom Slack message layout!
17
17
 
18
18
 
19
- # 📦 Installation
19
+ # 📦 Installation
20
20
 
21
21
  Run following commands:
22
22
 
@@ -43,7 +43,7 @@ Modify your `playwright.config.ts` file to include the following:
43
43
  ],
44
44
  ```
45
45
 
46
- Run your tests by providing your` SLACK_BOT_USER_OAUTH_TOKEN` as an environment variable:
46
+ Run your tests by providing your `SLACK_BOT_USER_OAUTH_TOKEN` as an environment variable or specifying `slackOAuthToken` option in the config:
47
47
 
48
48
  `SLACK_BOT_USER_OAUTH_TOKEN=[your Slack bot user OAUTH token] npx playwright test`
49
49
 
@@ -73,9 +73,9 @@ You will need to have Slack administrator rights to perform the steps below.
73
73
 
74
74
  ![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)
75
75
 
76
- * chat:write
77
- * chat:write.public
78
- * chat:write.customize
76
+ * chat:write
77
+ * chat:write.public
78
+ * chat:write.customize
79
79
 
80
80
  6. Scroll up to the OAuth Tokens for Your Workspace and click the **Install to Workspace** button
81
81
 
@@ -85,7 +85,7 @@ You will need to have Slack administrator rights to perform the steps below.
85
85
 
86
86
  ![click the Allow button](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-09_5-49-49.png?raw=true)
87
87
 
88
- The final step will be to copy the generated Bot User OAuth Token aka `SLACK_BOT_USER_OAUTH_TOKEN`.
88
+ The final step will be to copy the generated Bot User OAuth Token aka `SLACK_BOT_USER_OAUTH_TOKEN`.
89
89
 
90
90
  >**Treat this token as a secret.**
91
91
 
@@ -102,7 +102,7 @@ An example advanced configuration is shown below:
102
102
 
103
103
  ```typescript
104
104
  import { generateCustomLayout } from "./my_custom_layout";
105
-
105
+ import { LogLevel } from '@slack/web-api';
106
106
  ...
107
107
 
108
108
  reporter: [
@@ -127,14 +127,16 @@ An example advanced configuration is shown below:
127
127
  value: '<https://your-build-artifacts.my.company.dev/pw/23887/playwright-report/index.html|📊>',
128
128
  },
129
129
  ],
130
+ slackOAuthToken: 'YOUR_SLACK_OAUTH_TOKEN',
131
+ slackLogLevel: LogLevel.DEBUG
130
132
  },
131
-
133
+
132
134
  ],
133
135
  ],
134
136
  ```
135
137
 
136
138
  ### **channels**
137
- An array of Slack channels to post to, atleast one channel is required
139
+ An array of Slack channels to post to, at least one channel is required
138
140
  ### **sendResults**
139
141
  Can either be *"always"*, *"on-failure"* or *"off"*, this configuration is required:
140
142
  * **always** - will send the results to Slack at completion of the test run
@@ -143,8 +145,15 @@ Can either be *"always"*, *"on-failure"* or *"off"*, this configuration is requi
143
145
  ### **layout**
144
146
  A function that returns a layout object, this configuration is optional. See section below for more details.
145
147
  * meta - an array of meta data to be sent to Slack, this configuration is optional.
148
+ ### **layoutAsync**
149
+ Same as **layout** above, but asynchronous in that it returns a promise.
146
150
  ### **maxNumberOfFailuresToShow**
147
151
  Limits the number of failures shown in the Slack message, defaults to 10.
152
+ ### **slackOAuthToken**
153
+ Instead of providing an environment variable `SLACK_BOT_USER_OAUTH_TOKEN` you can specify the token in the config in the `slackOAuthToken` field.
154
+ ### **slackLogLevel** (default LogLevel.DEBUG)
155
+ This option allows you to control slack client severity levels for log entries. It accepts a value from @slack/web-api `LogLevel` enum
156
+
148
157
 
149
158
  **Examples:**
150
159
  ```typescript
@@ -162,7 +171,7 @@ meta: [
162
171
  key: 'GITHUB_REF',
163
172
  value: process.env.GITHUB_REF,
164
173
  },
165
- ],
174
+ ],
166
175
  ...
167
176
  ```
168
177
 
@@ -188,7 +197,7 @@ const generateCustomLayout = (summaryResults: SummaryResults):Array<KnownBlock |
188
197
  export default generateCustomLayout;
189
198
  ```
190
199
 
191
- In your, `playwright.confing.ts` file, add your function into the config.
200
+ In your, `playwright.config.ts` file, add your function into the config.
192
201
 
193
202
  ```typescript
194
203
  import { generateCustomLayout } from "./my_custom_layout";
@@ -260,7 +269,6 @@ Add the meta block in your config:
260
269
  },
261
270
  ],
262
271
  },
263
-
264
272
  ],
265
273
  ],
266
274
  ```
@@ -308,6 +316,204 @@ Generates the following message in Slack:
308
316
 
309
317
  ![Final](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/assets/2022-08-13_8-17-46.png?raw=true)
310
318
 
319
+
320
+ **Example 3: - with screenshots and/or recorded videos**
321
+
322
+ In your, `playwright.config.ts` file, add these params (Make sure you use **layoutAsync** rather than **layout**):
323
+
324
+ ```typescript
325
+ import { generateCustomLayoutAsync } from "./my_custom_layout";
326
+ ...
327
+ reporter: [
328
+ [
329
+ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js",
330
+ {
331
+ ...
332
+ layoutAsync: generateCustomLayoutAsync,
333
+ ...
334
+ },
335
+ ],
336
+ ],
337
+ use: {
338
+ ...
339
+ screenshot: "only-on-failure",
340
+ video: "retain-on-failure",
341
+ ...
342
+ },
343
+ ```
344
+
345
+ Create the function to generate the layout asynchronously in `my_custom_layout.ts`:
346
+
347
+ ```typescript
348
+ import fs from "fs";
349
+ import path from "path";
350
+ import { Block, KnownBlock } from "@slack/types";
351
+ import { SummaryResults } from "playwright-slack-report/dist/src";
352
+ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
353
+
354
+ const s3Client = new S3Client({
355
+ credentials: {
356
+ accessKeyId: process.env.S3_ACCESS_KEY || "",
357
+ secretAccessKey: process.env.S3_SECRET || "",
358
+ },
359
+ region: process.env.S3_REGION,
360
+ });
361
+
362
+ async function uploadFile(filePath, fileName) {
363
+ try {
364
+ const ext = path.extname(filePath);
365
+ const name = `${fileName}${ext}`;
366
+
367
+ await s3Client.send(
368
+ new PutObjectCommand({
369
+ Bucket: process.env.S3_BUCKET,
370
+ Key: name,
371
+ Body: fs.createReadStream(filePath),
372
+ })
373
+ );
374
+
375
+ return `https://${process.env.S3_BUCKET}.s3.${process.env.S3_REGION}.amazonaws.com/${name}`;
376
+ } catch (err) {
377
+ console.log("🔥🔥 Error", err);
378
+ }
379
+ }
380
+
381
+
382
+ export async function generateCustomLayoutAsync (summaryResults: SummaryResults): Promise<Array<KnownBlock | Block>> {
383
+ const { tests } = summaryResults;
384
+ // create your custom slack blocks
385
+
386
+ const header = {
387
+ type: "header",
388
+ text: {
389
+ type: "plain_text",
390
+ text: "🎭 *Playwright E2E Test Results*",
391
+ emoji: true,
392
+ },
393
+ };
394
+
395
+ const summary = {
396
+ type: "section",
397
+ text: {
398
+ type: "mrkdwn",
399
+ text: `✅ *${summaryResults.passed}* | ❌ *${summaryResults.failed}* | ⏩ *${summaryResults.skipped}*`,
400
+ },
401
+ };
402
+
403
+ const fails: Array<KnownBlock | Block> = [];
404
+
405
+ for (const t of tests) {
406
+ if (t.status === "failed" || t.status === "timedOut") {
407
+
408
+ fails.push({
409
+ type: "section",
410
+ text: {
411
+ type: "mrkdwn",
412
+ text: `👎 *[${t.browser}] | ${t.suiteName.replace(/\W/gi, "-")}*`,
413
+ },
414
+ });
415
+
416
+ const assets: Array<string> = [];
417
+
418
+ if (t.attachments) {
419
+ for (const a of t.attachments) {
420
+ // Upload failed tests screenshots and videos to the service of your choice
421
+ // In my case I upload the to S3 bucket
422
+ const permalink = await uploadFile(
423
+ a.path,
424
+ `${t.suiteName}--${t.name}`.replace(/\W/gi, "-").toLowerCase()
425
+ );
426
+
427
+ if (permalink) {
428
+ let icon = "";
429
+ if (a.name === "screenshot") {
430
+ icon = "📸";
431
+ } else if (a.name === "video") {
432
+ icon = "🎥";
433
+ }
434
+
435
+ assets.push(`${icon} See the <${permalink}|${a.name}>`);
436
+ }
437
+ }
438
+ }
439
+
440
+ if (assets.length > 0) {
441
+ fails.push({
442
+ type: "context",
443
+ elements: [{ type: "mrkdwn", text: assets.join("\n") }],
444
+ });
445
+ }
446
+ }
447
+ }
448
+
449
+ return [header, summary, { type: "divider" }, ...fails]
450
+ }
451
+
452
+ ```
453
+
454
+ **Also you can upload the attachments to slack.** But it might be more expensive for you and also you'll have to extend the scope.
455
+
456
+ ```typescript
457
+ ...
458
+ const web_api_1 = require('@slack/web-api');
459
+ const slackClient = new web_api_1.WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN);
460
+
461
+ async function uploadFile(filePath) {
462
+ try {
463
+ const result = await slackClient.files.uploadV2({
464
+ channels: 'you_cannel_name',
465
+ file: fs.createReadStream(filePath),
466
+ filename: filePath.split('/').at(-1),
467
+ });
468
+
469
+ return result.file;
470
+ } catch (error) {
471
+ console.log('🔥🔥 error', error);
472
+ }
473
+ }
474
+
475
+ export async function generateCustomLayoutAsync (summaryResults: SummaryResults): Promise<Array<KnownBlock | Block>> {
476
+ const { tests } = summaryResults;
477
+ ....
478
+ // See the snippet above ^^^
479
+
480
+
481
+ if (t.attachments) {
482
+ for (const a of t.attachments) {
483
+ const file = await uploadFile(a.path);
484
+
485
+ if (file) {
486
+ if (a.name === 'screenshot' && file.permalink) {
487
+ fails.push({
488
+ alt_text: '',
489
+ image_url: file.permalink,
490
+ title: { type: 'plain_text', text: file.name || '' },
491
+ type: 'image',
492
+ });
493
+ }
494
+
495
+ if (a.name === 'video' && file.permalink) {
496
+ fails.push({
497
+ alt_text: '',
498
+ // NOTE:
499
+ // Slack requires thumbnail_url length to be more that 0
500
+ // Either set screenshot url as the thumbnail or add a placeholder image url
501
+ thumbnail_url: '',
502
+ title: { type: 'plain_text', text: file.name || '' },
503
+ type: 'video',
504
+ video_url: file.permalink,
505
+ });
506
+ }
507
+ }
508
+ }
509
+ }
510
+ ....
511
+
512
+ return [header, summary, { type: "divider" }, ...fails]
513
+ }
514
+
515
+ ```
516
+
311
517
  # 🔑 License
312
518
 
313
519
  [MIT](https://github.com/ryanrosello-og/playwright-slack-report/blob/main/LICENSE)
@@ -324,7 +530,7 @@ Run the tests using `npm run pw`
324
530
  Run `npm pack`
325
531
 
326
532
  Create a new playwright project using `yarn create playwright`
327
- Modify the `package.json` and a local dependancy to the generated `tgz` file
533
+ Modify the `package.json` and a local dependancy to the generated `tgz` file
328
534
 
329
535
  e.g.
330
536
 
@@ -1,4 +1,4 @@
1
- import { KnownBlock, Block } from '@slack/types';
2
- import { SummaryResults } from '.';
3
- declare const generateBlocks: (summaryResults: SummaryResults, maxNumberOfFailures: number) => Promise<Array<KnownBlock | Block>>;
4
- export default generateBlocks;
1
+ import { KnownBlock, Block } from '@slack/types';
2
+ import { SummaryResults } from '.';
3
+ declare const generateBlocks: (summaryResults: SummaryResults, maxNumberOfFailures: number) => Promise<Array<KnownBlock | Block>>;
4
+ export default generateBlocks;
@@ -1,69 +1,69 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const generateBlocks = async (summaryResults, maxNumberOfFailures) => {
4
- const maxNumberOfFailureLength = 650;
5
- const fails = [];
6
- const meta = [];
7
- const header = {
8
- type: 'section',
9
- text: {
10
- type: 'mrkdwn',
11
- text: '🎭 *Playwright Results*',
12
- },
13
- };
14
- const summary = {
15
- type: 'section',
16
- text: {
17
- type: 'mrkdwn',
18
- text: `✅ *${summaryResults.passed}* | ❌ *${summaryResults.failed}* | ⏩ *${summaryResults.skipped}*`,
19
- },
20
- };
21
- for (let i = 0; i < summaryResults.failures.length; i += 1) {
22
- const { failureReason, test } = summaryResults.failures[i];
23
- const formattedFailure = failureReason
24
- .substring(0, maxNumberOfFailureLength)
25
- .split('\n')
26
- .map((l) => `>${l}`)
27
- .join('\n');
28
- fails.push({
29
- type: 'section',
30
- text: {
31
- type: 'mrkdwn',
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const generateBlocks = async (summaryResults, maxNumberOfFailures) => {
4
+ const maxNumberOfFailureLength = 650;
5
+ const fails = [];
6
+ const meta = [];
7
+ const header = {
8
+ type: 'section',
9
+ text: {
10
+ type: 'mrkdwn',
11
+ text: '🎭 *Playwright Results*',
12
+ },
13
+ };
14
+ const summary = {
15
+ type: 'section',
16
+ text: {
17
+ type: 'mrkdwn',
18
+ text: `✅ *${summaryResults.passed}* | ❌ *${summaryResults.failed}* | ⏩ *${summaryResults.skipped}*`,
19
+ },
20
+ };
21
+ for (let i = 0; i < summaryResults.failures.length; i += 1) {
22
+ const { failureReason, test } = summaryResults.failures[i];
23
+ const formattedFailure = failureReason
24
+ .substring(0, maxNumberOfFailureLength)
25
+ .split('\n')
26
+ .map((l) => `>${l}`)
27
+ .join('\n');
28
+ fails.push({
29
+ type: 'section',
30
+ text: {
31
+ type: 'mrkdwn',
32
32
  text: `*${test}*
33
- \n${formattedFailure}`,
34
- },
35
- });
36
- if (i > maxNumberOfFailures) {
37
- fails.push({
38
- type: 'section',
39
- text: {
40
- type: 'mrkdwn',
41
- text: `*There are too many failures to display - ${fails.length} out of ${summaryResults.failures.length} failures shown*`,
42
- },
43
- });
44
- break;
45
- }
46
- }
47
- if (summaryResults.meta) {
48
- for (let i = 0; i < summaryResults.meta.length; i += 1) {
49
- const { key, value } = summaryResults.meta[i];
50
- meta.push({
51
- type: 'section',
52
- text: {
53
- type: 'mrkdwn',
54
- text: `\n*${key}* :\t${value}`,
55
- },
56
- });
57
- }
58
- }
59
- return [
60
- header,
61
- summary,
62
- ...meta,
63
- {
64
- type: 'divider',
65
- },
66
- ...fails,
67
- ];
68
- };
69
- exports.default = generateBlocks;
33
+ \n${formattedFailure}`,
34
+ },
35
+ });
36
+ if (i > maxNumberOfFailures) {
37
+ fails.push({
38
+ type: 'section',
39
+ text: {
40
+ type: 'mrkdwn',
41
+ text: `*There are too many failures to display - ${fails.length} out of ${summaryResults.failures.length} failures shown*`,
42
+ },
43
+ });
44
+ break;
45
+ }
46
+ }
47
+ if (summaryResults.meta) {
48
+ for (let i = 0; i < summaryResults.meta.length; i += 1) {
49
+ const { key, value } = summaryResults.meta[i];
50
+ meta.push({
51
+ type: 'section',
52
+ text: {
53
+ type: 'mrkdwn',
54
+ text: `\n*${key}* :\t${value}`,
55
+ },
56
+ });
57
+ }
58
+ }
59
+ return [
60
+ header,
61
+ summary,
62
+ ...meta,
63
+ {
64
+ type: 'divider',
65
+ },
66
+ ...fails,
67
+ ];
68
+ };
69
+ exports.default = generateBlocks;
@@ -1,46 +1,46 @@
1
- /// <reference types="node" />
2
- import { failure, SummaryResults } from '.';
3
- export declare type testResult = {
4
- suiteName: string;
5
- name: string;
6
- browser?: string;
7
- projectName: string;
8
- endedAt: string;
9
- reason: string;
10
- retry: number;
11
- retries: number;
12
- startedAt: string;
13
- status: 'passed' | 'failed' | 'timedOut' | 'skipped';
14
- attachments?: {
15
- body: string | undefined | Buffer;
16
- contentType: string;
17
- name: string;
18
- path: string;
19
- }[];
20
- };
21
- export declare type testSuite = {
22
- testSuite: {
23
- title: string;
24
- tests: testResult[];
25
- testRunId?: number;
26
- };
27
- };
28
- export default class ResultsParser {
29
- private result;
30
- constructor();
31
- getParsedResults(): Promise<SummaryResults>;
32
- getFailures(): Promise<Array<failure>>;
33
- static getTestName(failedTest: any): any;
34
- updateResults(data: {
35
- testSuite: any;
36
- }): void;
37
- addTestResult(suiteName: any, testCase: any): void;
38
- safelyDetermineFailure(result: {
39
- errors: any[];
40
- error: {
41
- message: string;
42
- stack: string;
43
- };
44
- }): string;
45
- cleanseReason(rawReaseon: string): string;
46
- }
1
+ /// <reference types="node" />
2
+ import { failure, SummaryResults } from '.';
3
+ export declare type testResult = {
4
+ suiteName: string;
5
+ name: string;
6
+ browser?: string;
7
+ projectName: string;
8
+ endedAt: string;
9
+ reason: string;
10
+ retry: number;
11
+ retries: number;
12
+ startedAt: string;
13
+ status: 'passed' | 'failed' | 'timedOut' | 'skipped';
14
+ attachments?: {
15
+ body: string | undefined | Buffer;
16
+ contentType: string;
17
+ name: string;
18
+ path: string;
19
+ }[];
20
+ };
21
+ export declare type testSuite = {
22
+ testSuite: {
23
+ title: string;
24
+ tests: testResult[];
25
+ testRunId?: number;
26
+ };
27
+ };
28
+ export default class ResultsParser {
29
+ private result;
30
+ constructor();
31
+ getParsedResults(): Promise<SummaryResults>;
32
+ getFailures(): Promise<Array<failure>>;
33
+ static getTestName(failedTest: any): any;
34
+ updateResults(data: {
35
+ testSuite: any;
36
+ }): void;
37
+ addTestResult(suiteName: any, testCase: any): void;
38
+ safelyDetermineFailure(result: {
39
+ errors: any[];
40
+ error: {
41
+ message: string;
42
+ stack: string;
43
+ };
44
+ }): string;
45
+ cleanseReason(rawReaseon: string): string;
46
+ }