playwright-slack-report-burak 1.2.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/LICENSE +21 -0
- package/README.md +614 -0
- package/dist/src/LayoutGenerator.d.ts +5 -0
- package/dist/src/LayoutGenerator.js +68 -0
- package/dist/src/ResultsParser.d.ts +62 -0
- package/dist/src/ResultsParser.js +187 -0
- package/dist/src/SlackClient.d.ts +37 -0
- package/dist/src/SlackClient.js +101 -0
- package/dist/src/SlackReporter.d.ts +30 -0
- package/dist/src/SlackReporter.js +189 -0
- package/dist/src/SlackWebhookClient.d.ts +15 -0
- package/dist/src/SlackWebhookClient.js +42 -0
- package/dist/src/custom_block/my_block.d.ts +4 -0
- package/dist/src/custom_block/my_block.js +87 -0
- package/dist/src/custom_block/simple.d.ts +3 -0
- package/dist/src/custom_block/simple.js +16 -0
- package/dist/src/custom_block/simple_with_meta.d.ts +3 -0
- package/dist/src/custom_block/simple_with_meta.js +30 -0
- package/dist/src/index.d.ts +54 -0
- package/dist/src/index.js +2 -0
- package/package.json +48 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { failure, flaky, pass, skipper, 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
|
+
private separateFlakyTests;
|
|
31
|
+
constructor(options?: {
|
|
32
|
+
separateFlakyTests: boolean;
|
|
33
|
+
});
|
|
34
|
+
getParsedResults(): Promise<SummaryResults>;
|
|
35
|
+
getFailures(): Promise<Array<failure>>;
|
|
36
|
+
getFlakes(): Promise<Array<flaky>>;
|
|
37
|
+
getPasses(): Promise<Array<pass>>;
|
|
38
|
+
getSkippers(): Promise<Array<skipper>>;
|
|
39
|
+
static getTestName(failedTest: any): any;
|
|
40
|
+
updateResults(data: {
|
|
41
|
+
testSuite: any;
|
|
42
|
+
}): void;
|
|
43
|
+
addTestResult(suiteName: any, testCase: any, projectBrowserMapping: any): void;
|
|
44
|
+
safelyDetermineFailure(result: {
|
|
45
|
+
errors: any[];
|
|
46
|
+
error: {
|
|
47
|
+
message: string;
|
|
48
|
+
stack: string;
|
|
49
|
+
};
|
|
50
|
+
}): string;
|
|
51
|
+
cleanseReason(rawReaseon: string): string;
|
|
52
|
+
determineBrowser(projectName: string, browserMappings: {
|
|
53
|
+
projectName: string;
|
|
54
|
+
browser: string;
|
|
55
|
+
}[]): {
|
|
56
|
+
projectName: string;
|
|
57
|
+
browser: string;
|
|
58
|
+
};
|
|
59
|
+
/** removes tests from the passed array that only passed on a retry (flaky).
|
|
60
|
+
* Does not modify param passed, returns a new passed array. */
|
|
61
|
+
doSeparateFlakyTests(passes: Array<pass>, flakes: Array<flaky>): pass[];
|
|
62
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable no-shadow */
|
|
3
|
+
/* eslint-disable no-underscore-dangle */
|
|
4
|
+
/* eslint-disable import/extensions */
|
|
5
|
+
/* eslint-disable no-control-regex */
|
|
6
|
+
/* eslint-disable class-methods-use-this */
|
|
7
|
+
/* eslint-disable no-param-reassign */
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
class ResultsParser {
|
|
10
|
+
result;
|
|
11
|
+
separateFlakyTests;
|
|
12
|
+
constructor(options = { separateFlakyTests: false }) {
|
|
13
|
+
this.result = [];
|
|
14
|
+
this.separateFlakyTests = options.separateFlakyTests;
|
|
15
|
+
}
|
|
16
|
+
async getParsedResults() {
|
|
17
|
+
const failures = await this.getFailures();
|
|
18
|
+
const flakes = await this.getFlakes();
|
|
19
|
+
let passes = await this.getPasses();
|
|
20
|
+
const skippers = await this.getSkippers();
|
|
21
|
+
if (this.separateFlakyTests) {
|
|
22
|
+
passes = this.doSeparateFlakyTests(passes, flakes);
|
|
23
|
+
}
|
|
24
|
+
const summary = {
|
|
25
|
+
passed: passes.length,
|
|
26
|
+
failed: failures.length,
|
|
27
|
+
flaky: this.separateFlakyTests ? flakes.length : undefined,
|
|
28
|
+
skipped: skippers.length,
|
|
29
|
+
failures,
|
|
30
|
+
tests: [],
|
|
31
|
+
};
|
|
32
|
+
for (const suite of this.result) {
|
|
33
|
+
summary.tests = summary.tests.concat(suite.testSuite.tests);
|
|
34
|
+
for (const test of suite.testSuite.tests) {
|
|
35
|
+
if (test.status === 'skipped') {
|
|
36
|
+
summary.skipped += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return summary;
|
|
41
|
+
}
|
|
42
|
+
async getFailures() {
|
|
43
|
+
const failures = [];
|
|
44
|
+
for (const suite of this.result) {
|
|
45
|
+
for (const test of suite.testSuite.tests) {
|
|
46
|
+
if (test.status === 'failed' || test.status === 'timedOut') {
|
|
47
|
+
// only flag as failed if the last attempt has failed
|
|
48
|
+
if (test.retries === test.retry) {
|
|
49
|
+
failures.push({
|
|
50
|
+
test: ResultsParser.getTestName(test),
|
|
51
|
+
failureReason: test.reason,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return failures;
|
|
58
|
+
}
|
|
59
|
+
async getFlakes() {
|
|
60
|
+
const flaky = [];
|
|
61
|
+
for (const suite of this.result) {
|
|
62
|
+
for (const test of suite.testSuite.tests) {
|
|
63
|
+
if (test.status === 'passed' && test.retry > 0) {
|
|
64
|
+
flaky.push({
|
|
65
|
+
test: ResultsParser.getTestName(test),
|
|
66
|
+
retry: test.retry,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return flaky;
|
|
72
|
+
}
|
|
73
|
+
async getPasses() {
|
|
74
|
+
const passes = [];
|
|
75
|
+
for (const suite of this.result) {
|
|
76
|
+
for (const test of suite.testSuite.tests) {
|
|
77
|
+
if (test.status === 'passed') {
|
|
78
|
+
passes.push({
|
|
79
|
+
test: ResultsParser.getTestName(test),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return passes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getSkippers() {
|
|
88
|
+
const skippers = [];
|
|
89
|
+
for (const suite of this.result) {
|
|
90
|
+
for (const test of suite.testSuite.tests) {
|
|
91
|
+
if (test.status === 'skipped') {
|
|
92
|
+
skippers.push({
|
|
93
|
+
test: ResultsParser.getTestName(test),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return skippers;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
static getTestName(failedTest) {
|
|
103
|
+
const testName = failedTest.name;
|
|
104
|
+
if (failedTest.browser && failedTest.projectName) {
|
|
105
|
+
if (failedTest.browser === failedTest.projectName) {
|
|
106
|
+
return `${testName} [${failedTest.browser}]`;
|
|
107
|
+
}
|
|
108
|
+
return `${testName} [Project Name: ${failedTest.projectName}] using ${failedTest.browser}`;
|
|
109
|
+
}
|
|
110
|
+
return testName;
|
|
111
|
+
}
|
|
112
|
+
updateResults(data) {
|
|
113
|
+
if (data.testSuite.tests.length > 0) {
|
|
114
|
+
this.result.push(data);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
addTestResult(suiteName, testCase, projectBrowserMapping) {
|
|
118
|
+
const testResults = [];
|
|
119
|
+
const projectSettings = this.determineBrowser(testCase._projectId, projectBrowserMapping);
|
|
120
|
+
for (const result of testCase.results) {
|
|
121
|
+
testResults.push({
|
|
122
|
+
suiteName,
|
|
123
|
+
name: testCase.title,
|
|
124
|
+
status: result.status,
|
|
125
|
+
browser: projectSettings.browser,
|
|
126
|
+
projectName: projectSettings.projectName,
|
|
127
|
+
retry: result.retry,
|
|
128
|
+
retries: testCase.retries,
|
|
129
|
+
startedAt: new Date(result.startTime).toISOString(),
|
|
130
|
+
endedAt: new Date(new Date(result.startTime).getTime() + result.duration).toISOString(),
|
|
131
|
+
reason: this.safelyDetermineFailure(result),
|
|
132
|
+
attachments: result.attachments,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
this.updateResults({
|
|
136
|
+
testSuite: {
|
|
137
|
+
title: suiteName,
|
|
138
|
+
tests: testResults,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
safelyDetermineFailure(result) {
|
|
143
|
+
if (result.errors.length > 0) {
|
|
144
|
+
const fullError = result.errors
|
|
145
|
+
.map((e) => `${e.message}\r\n${e.stack ? e.stack : ''}\r\n`)
|
|
146
|
+
.join();
|
|
147
|
+
return this.cleanseReason(fullError);
|
|
148
|
+
}
|
|
149
|
+
return `${this.cleanseReason(result.error?.message)} \n ${this.cleanseReason(result.error?.stack)}`;
|
|
150
|
+
}
|
|
151
|
+
cleanseReason(rawReaseon) {
|
|
152
|
+
// eslint-disable-next-line prefer-regex-literals
|
|
153
|
+
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');
|
|
154
|
+
const ansiCleansed = rawReaseon ? rawReaseon.replace(ansiRegex, '') : '';
|
|
155
|
+
const logsStripped = ansiCleansed
|
|
156
|
+
.replace(/============================================================\n/g, '')
|
|
157
|
+
.replace(/============================================================\r\n/g, '')
|
|
158
|
+
.replace(/=========================== logs ===========================\n/g, '');
|
|
159
|
+
return logsStripped;
|
|
160
|
+
}
|
|
161
|
+
determineBrowser(projectName, browserMappings) {
|
|
162
|
+
const browserMapping = browserMappings.find((mapping) => mapping.projectName === projectName);
|
|
163
|
+
if (browserMapping) {
|
|
164
|
+
return {
|
|
165
|
+
projectName: browserMapping.projectName,
|
|
166
|
+
browser: browserMapping.browser,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
projectName: '',
|
|
171
|
+
browser: '',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** removes tests from the passed array that only passed on a retry (flaky).
|
|
175
|
+
* Does not modify param passed, returns a new passed array. */
|
|
176
|
+
doSeparateFlakyTests(passes, flakes) {
|
|
177
|
+
const _passes = new Map();
|
|
178
|
+
for (const pass of passes) {
|
|
179
|
+
_passes.set(pass.test, pass);
|
|
180
|
+
}
|
|
181
|
+
for (const flake of flakes) {
|
|
182
|
+
_passes.delete(flake.test);
|
|
183
|
+
}
|
|
184
|
+
return [..._passes.values()];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.default = ResultsParser;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { WebClient, KnownBlock, Block, ChatPostMessageResponse, LogLevel } 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
|
+
customLayout: Function | undefined;
|
|
14
|
+
customLayoutAsync: Function | undefined;
|
|
15
|
+
fakeRequest?: Function;
|
|
16
|
+
maxNumberOfFailures: number;
|
|
17
|
+
slackOAuthToken?: string;
|
|
18
|
+
slackLogLevel?: LogLevel;
|
|
19
|
+
disableUnfurl?: boolean;
|
|
20
|
+
summaryResults: SummaryResults;
|
|
21
|
+
showInThread: boolean;
|
|
22
|
+
};
|
|
23
|
+
}): Promise<Array<{
|
|
24
|
+
channel: string;
|
|
25
|
+
outcome: string;
|
|
26
|
+
ts: string;
|
|
27
|
+
}>>;
|
|
28
|
+
attachDetailsToThread({ channelIds, ts, summaryResults, maxNumberOfFailures, disableUnfurl, fakeRequest, }: {
|
|
29
|
+
channelIds: Array<string>;
|
|
30
|
+
ts: string;
|
|
31
|
+
summaryResults: SummaryResults;
|
|
32
|
+
maxNumberOfFailures: number;
|
|
33
|
+
disableUnfurl?: boolean;
|
|
34
|
+
fakeRequest?: Function;
|
|
35
|
+
}): Promise<any[]>;
|
|
36
|
+
static doPostRequest(slackWebClient: WebClient, channel: string, blocks: Array<KnownBlock | Block>, unfurl: boolean, threadTimestamp?: string): Promise<ChatPostMessageResponse>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 if (options.customLayoutAsync) {
|
|
15
|
+
blocks = await options.customLayoutAsync(options.summaryResults);
|
|
16
|
+
}
|
|
17
|
+
else if (options.showInThread) {
|
|
18
|
+
const modifiedOptions = JSON.parse(JSON.stringify(options));
|
|
19
|
+
modifiedOptions.summaryResults.failures = [];
|
|
20
|
+
blocks = await (0, LayoutGenerator_1.generateBlocks)(modifiedOptions.summaryResults, options.maxNumberOfFailures);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
blocks = await (0, LayoutGenerator_1.generateBlocks)(options.summaryResults, options.maxNumberOfFailures);
|
|
24
|
+
}
|
|
25
|
+
if (!options.channelIds) {
|
|
26
|
+
throw new Error(`Channel ids [${options.channelIds}] is not valid`);
|
|
27
|
+
}
|
|
28
|
+
const result = [];
|
|
29
|
+
const unfurl = !options.disableUnfurl;
|
|
30
|
+
for (const channel of options.channelIds) {
|
|
31
|
+
let chatResponse;
|
|
32
|
+
try {
|
|
33
|
+
// under test
|
|
34
|
+
if (options.fakeRequest) {
|
|
35
|
+
chatResponse = await options.fakeRequest();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// send request for reals
|
|
39
|
+
chatResponse = await SlackClient.doPostRequest(this.slackWebClient, channel, blocks, unfurl);
|
|
40
|
+
}
|
|
41
|
+
if (chatResponse.ok) {
|
|
42
|
+
result.push({
|
|
43
|
+
channel,
|
|
44
|
+
outcome: `✅ Message sent to ${channel}`,
|
|
45
|
+
ts: chatResponse.ts,
|
|
46
|
+
});
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(`✅ Message sent to ${channel}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
result.push({
|
|
52
|
+
channel,
|
|
53
|
+
outcome: `❌ Message not sent to ${channel} \r\n ${JSON.stringify(chatResponse, null, 2)}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
result.push({
|
|
59
|
+
channel,
|
|
60
|
+
outcome: `❌ Message not sent to ${channel} \r\n ${error.message}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
async attachDetailsToThread({ channelIds, ts, summaryResults, maxNumberOfFailures, disableUnfurl, fakeRequest, }) {
|
|
67
|
+
const result = [];
|
|
68
|
+
const blocks = await (0, LayoutGenerator_1.generateFailures)(summaryResults, maxNumberOfFailures);
|
|
69
|
+
for (const channel of channelIds) {
|
|
70
|
+
// under test
|
|
71
|
+
let chatResponse;
|
|
72
|
+
if (fakeRequest) {
|
|
73
|
+
chatResponse = await fakeRequest();
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
chatResponse = await SlackClient.doPostRequest(this.slackWebClient, channel, blocks, disableUnfurl, ts);
|
|
77
|
+
}
|
|
78
|
+
if (chatResponse.ok) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.log(`✅ Message sent to ${channel} within thread ${ts}`);
|
|
81
|
+
result.push({
|
|
82
|
+
channel,
|
|
83
|
+
outcome: `✅ Message sent to ${channel} within thread ${ts}`,
|
|
84
|
+
ts: chatResponse.ts,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
static async doPostRequest(slackWebClient, channel, blocks, unfurl, threadTimestamp) {
|
|
91
|
+
const chatResponse = await slackWebClient.chat.postMessage({
|
|
92
|
+
channel,
|
|
93
|
+
text: ' ',
|
|
94
|
+
unfurl_link: unfurl,
|
|
95
|
+
blocks,
|
|
96
|
+
thread_ts: threadTimestamp,
|
|
97
|
+
});
|
|
98
|
+
return chatResponse;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.default = SlackClient;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FullConfig, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
|
|
2
|
+
declare class SlackReporter implements Reporter {
|
|
3
|
+
private customLayout;
|
|
4
|
+
private customLayoutAsync;
|
|
5
|
+
private maxNumberOfFailuresToShow;
|
|
6
|
+
private showInThread;
|
|
7
|
+
private meta;
|
|
8
|
+
private resultsParser;
|
|
9
|
+
private sendResults;
|
|
10
|
+
private slackChannels;
|
|
11
|
+
private slackLogLevel;
|
|
12
|
+
private slackOAuthToken;
|
|
13
|
+
private slackWebHookUrl;
|
|
14
|
+
private disableUnfurl;
|
|
15
|
+
private proxy;
|
|
16
|
+
private browsers;
|
|
17
|
+
private suite;
|
|
18
|
+
private separateFlaky;
|
|
19
|
+
logs: string[];
|
|
20
|
+
onBegin(fullConfig: FullConfig, suite: Suite): void;
|
|
21
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
22
|
+
onEnd(): Promise<void>;
|
|
23
|
+
preChecks(): {
|
|
24
|
+
okToProceed: boolean;
|
|
25
|
+
message?: string;
|
|
26
|
+
};
|
|
27
|
+
log(message: string | undefined): void;
|
|
28
|
+
printsToStdio(): boolean;
|
|
29
|
+
}
|
|
30
|
+
export default SlackReporter;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const web_api_1 = require("@slack/web-api");
|
|
4
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
5
|
+
const webhook_1 = require("@slack/webhook");
|
|
6
|
+
const ResultsParser_1 = require("./ResultsParser");
|
|
7
|
+
const SlackClient_1 = require("./SlackClient");
|
|
8
|
+
const SlackWebhookClient_1 = require("./SlackWebhookClient");
|
|
9
|
+
class SlackReporter {
|
|
10
|
+
customLayout;
|
|
11
|
+
customLayoutAsync;
|
|
12
|
+
maxNumberOfFailuresToShow;
|
|
13
|
+
showInThread;
|
|
14
|
+
meta = [];
|
|
15
|
+
resultsParser;
|
|
16
|
+
sendResults = 'on-failure';
|
|
17
|
+
slackChannels = [];
|
|
18
|
+
slackLogLevel;
|
|
19
|
+
slackOAuthToken;
|
|
20
|
+
slackWebHookUrl;
|
|
21
|
+
disableUnfurl;
|
|
22
|
+
proxy;
|
|
23
|
+
browsers = [];
|
|
24
|
+
suite;
|
|
25
|
+
separateFlaky = false;
|
|
26
|
+
logs = [];
|
|
27
|
+
onBegin(fullConfig, suite) {
|
|
28
|
+
this.suite = suite;
|
|
29
|
+
this.logs = [];
|
|
30
|
+
const slackReporterConfig = fullConfig.reporter.filter((f) => f[0].toLowerCase().includes('slackreporter'))[0][1];
|
|
31
|
+
if (fullConfig.projects.length === 0) {
|
|
32
|
+
this.browsers = [];
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// eslint-disable-next-line max-len
|
|
36
|
+
this.browsers = fullConfig.projects.map((obj) => ({
|
|
37
|
+
projectName: obj.name,
|
|
38
|
+
browser: obj.use.browserName
|
|
39
|
+
? obj.use.browserName
|
|
40
|
+
: obj.use.defaultBrowserType,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
if (slackReporterConfig) {
|
|
44
|
+
this.meta = slackReporterConfig.meta || [];
|
|
45
|
+
this.sendResults = slackReporterConfig.sendResults || 'always';
|
|
46
|
+
this.customLayout = slackReporterConfig.layout;
|
|
47
|
+
this.customLayoutAsync = slackReporterConfig.layoutAsync;
|
|
48
|
+
this.slackChannels = slackReporterConfig.channels;
|
|
49
|
+
this.maxNumberOfFailuresToShow
|
|
50
|
+
= slackReporterConfig.maxNumberOfFailuresToShow || 10;
|
|
51
|
+
this.slackOAuthToken = slackReporterConfig.slackOAuthToken || undefined;
|
|
52
|
+
this.slackWebHookUrl = slackReporterConfig.slackWebHookUrl || undefined;
|
|
53
|
+
this.disableUnfurl = slackReporterConfig.disableUnfurl || false;
|
|
54
|
+
this.showInThread = slackReporterConfig.showInThread || false;
|
|
55
|
+
this.slackLogLevel = slackReporterConfig.slackLogLevel || web_api_1.LogLevel.DEBUG;
|
|
56
|
+
this.proxy = slackReporterConfig.proxy || undefined;
|
|
57
|
+
this.separateFlaky = slackReporterConfig.separateFlaky || false;
|
|
58
|
+
}
|
|
59
|
+
this.resultsParser = new ResultsParser_1.default({
|
|
60
|
+
separateFlakyTests: this.separateFlaky,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
|
64
|
+
onTestEnd(test, result) {
|
|
65
|
+
this.resultsParser.addTestResult(test.parent.title, test, this.browsers);
|
|
66
|
+
}
|
|
67
|
+
async onEnd() {
|
|
68
|
+
const { okToProceed, message } = this.preChecks();
|
|
69
|
+
if (!okToProceed) {
|
|
70
|
+
this.log(message);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const resultSummary = await this.resultsParser.getParsedResults();
|
|
74
|
+
resultSummary.meta = this.meta;
|
|
75
|
+
const maxRetry = Math.max(...resultSummary.tests.map((o) => o.retry));
|
|
76
|
+
if (this.sendResults === 'on-failure'
|
|
77
|
+
&& resultSummary.tests.filter((z) => (z.status === 'failed' || z.status === 'timedOut')
|
|
78
|
+
&& z.retry === maxRetry).length === 0) {
|
|
79
|
+
this.log('⏩ Slack reporter - no failures found');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const agent = this.proxy ? new https_proxy_agent_1.HttpsProxyAgent(this.proxy) : undefined;
|
|
83
|
+
if (this.slackWebHookUrl) {
|
|
84
|
+
const webhook = new webhook_1.IncomingWebhook(this.slackWebHookUrl, { agent });
|
|
85
|
+
const slackWebhookClient = new SlackWebhookClient_1.default(webhook);
|
|
86
|
+
const webhookResult = await slackWebhookClient.sendMessage({
|
|
87
|
+
customLayout: this.customLayout,
|
|
88
|
+
customLayoutAsync: this.customLayoutAsync,
|
|
89
|
+
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
|
|
90
|
+
disableUnfurl: this.disableUnfurl,
|
|
91
|
+
summaryResults: resultSummary,
|
|
92
|
+
});
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.log(JSON.stringify(webhookResult, null, 2));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const slackClient = new SlackClient_1.default(new web_api_1.WebClient(this.slackOAuthToken || process.env.SLACK_BOT_USER_OAUTH_TOKEN, {
|
|
98
|
+
logLevel: this.slackLogLevel || web_api_1.LogLevel.DEBUG,
|
|
99
|
+
agent,
|
|
100
|
+
}));
|
|
101
|
+
const result = await slackClient.sendMessage({
|
|
102
|
+
options: {
|
|
103
|
+
channelIds: this.slackChannels,
|
|
104
|
+
customLayout: this.customLayout,
|
|
105
|
+
customLayoutAsync: this.customLayoutAsync,
|
|
106
|
+
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
|
|
107
|
+
disableUnfurl: this.disableUnfurl,
|
|
108
|
+
summaryResults: resultSummary,
|
|
109
|
+
showInThread: this.showInThread,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
// eslint-disable-next-line no-console
|
|
113
|
+
console.log(JSON.stringify(result, null, 2));
|
|
114
|
+
if (this.showInThread && resultSummary.failures.length > 0) {
|
|
115
|
+
for (let i = 0; i < result.length; i += 1) {
|
|
116
|
+
// eslint-disable-next-line no-await-in-loop
|
|
117
|
+
await slackClient.attachDetailsToThread({
|
|
118
|
+
channelIds: [result[i].channel],
|
|
119
|
+
ts: result[i].ts,
|
|
120
|
+
summaryResults: resultSummary,
|
|
121
|
+
maxNumberOfFailures: this.maxNumberOfFailuresToShow,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
preChecks() {
|
|
128
|
+
if (this.sendResults === 'off') {
|
|
129
|
+
return { okToProceed: false, message: '❌ Slack reporter is disabled' };
|
|
130
|
+
}
|
|
131
|
+
if (!this.slackWebHookUrl
|
|
132
|
+
&& !this.slackOAuthToken
|
|
133
|
+
&& !process.env.SLACK_BOT_USER_OAUTH_TOKEN) {
|
|
134
|
+
return {
|
|
135
|
+
okToProceed: false,
|
|
136
|
+
message: '❌ Neither slack webhook url, slackOAuthToken nor process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (this.slackWebHookUrl
|
|
140
|
+
&& (process.env.SLACK_BOT_USER_OAUTH_TOKEN || this.slackOAuthToken)) {
|
|
141
|
+
return {
|
|
142
|
+
okToProceed: false,
|
|
143
|
+
message: '❌ You can only enable a single option, either provide a slack webhook url, slackOAuthToken or process.env.SLACK_BOT_USER_OAUTH_TOKEN were found',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!this.sendResults
|
|
147
|
+
|| !['always', 'on-failure', 'off'].includes(this.sendResults)) {
|
|
148
|
+
return {
|
|
149
|
+
okToProceed: false,
|
|
150
|
+
message: "❌ \"sendResults\" is not valid. Expecting one of ['always', 'on-failure', 'off'].",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (!this.sendResults || this.slackChannels?.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
okToProceed: false,
|
|
156
|
+
message: '❌ Slack channel(s) was not provided in the config',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (this.customLayout && typeof this.customLayout !== 'function') {
|
|
160
|
+
return {
|
|
161
|
+
okToProceed: false,
|
|
162
|
+
message: '❌ Custom layout is not a function',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (this.customLayoutAsync
|
|
166
|
+
&& typeof this.customLayoutAsync !== 'function') {
|
|
167
|
+
return {
|
|
168
|
+
okToProceed: false,
|
|
169
|
+
message: '❌ customLayoutAsync is not a function',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (this.meta && !Array.isArray(this.meta)) {
|
|
173
|
+
return { okToProceed: false, message: '❌ Meta is not an array' };
|
|
174
|
+
}
|
|
175
|
+
return { okToProceed: true };
|
|
176
|
+
}
|
|
177
|
+
log(message) {
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log(message);
|
|
180
|
+
if (message) {
|
|
181
|
+
this.logs.push(message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// eslint-disable-next-line class-methods-use-this
|
|
185
|
+
printsToStdio() {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.default = SlackReporter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IncomingWebhook } from '@slack/webhook';
|
|
2
|
+
import { SummaryResults } from '.';
|
|
3
|
+
export default class SlackWebhookClient {
|
|
4
|
+
private webhook;
|
|
5
|
+
constructor(webhook: IncomingWebhook);
|
|
6
|
+
sendMessage({ customLayout, customLayoutAsync, maxNumberOfFailures, summaryResults, disableUnfurl, }: {
|
|
7
|
+
customLayout: Function | undefined;
|
|
8
|
+
customLayoutAsync: Function | undefined;
|
|
9
|
+
maxNumberOfFailures: number;
|
|
10
|
+
summaryResults: SummaryResults;
|
|
11
|
+
disableUnfurl: boolean;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
outcome: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const LayoutGenerator_1 = require("./LayoutGenerator");
|
|
4
|
+
class SlackWebhookClient {
|
|
5
|
+
webhook;
|
|
6
|
+
constructor(webhook) {
|
|
7
|
+
this.webhook = webhook;
|
|
8
|
+
}
|
|
9
|
+
async sendMessage({ customLayout, customLayoutAsync, maxNumberOfFailures, summaryResults, disableUnfurl, }) {
|
|
10
|
+
let blocks;
|
|
11
|
+
if (customLayout) {
|
|
12
|
+
blocks = customLayout(summaryResults);
|
|
13
|
+
}
|
|
14
|
+
else if (customLayoutAsync) {
|
|
15
|
+
blocks = await customLayoutAsync(summaryResults);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
blocks = await (0, LayoutGenerator_1.generateBlocks)(summaryResults, maxNumberOfFailures);
|
|
19
|
+
}
|
|
20
|
+
let result;
|
|
21
|
+
try {
|
|
22
|
+
result = await this.webhook.send({
|
|
23
|
+
blocks,
|
|
24
|
+
unfurl_links: !disableUnfurl,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
outcome: `error: ${JSON.stringify(error, null, 2)}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (result && result.text === 'ok') {
|
|
33
|
+
return {
|
|
34
|
+
outcome: result.text,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
outcome: '😵 Failed to send webhook message, ensure your webhook url is valid',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.default = SlackWebhookClient;
|