Adds Jira integration for test results
Adds a Jira writeback reporter to automatically create bug and test case subtasks in Jira based on playwright test results. Upgrades axios and adds axios-retry and axios-mock-adapter for more robust API calls.
This commit is contained in:
parent
b2d0038e45
commit
219762ec30
5 changed files with 726 additions and 6 deletions
135
package-lock.json
generated
135
package-lock.json
generated
|
|
@ -9,7 +9,6 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@iframe-resizer/child": "^5.3.3",
|
||||
"axios": "^0.23.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"core-js": "^3.38.1",
|
||||
"http-status-codes": "^2.3.0",
|
||||
|
|
@ -38,6 +37,9 @@
|
|||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^27.0.0",
|
||||
"axios": "^1.8.4",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"dotenv-safe": "^9.1.0",
|
||||
"eslint": "8.57",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
|
|
@ -5412,12 +5414,82 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-mock-adapter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz",
|
||||
"integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"is-buffer": "^2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": ">= 0.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-mock-adapter/node_modules/is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz",
|
||||
"integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-retry-allowed": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "0.x || 1.x"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
|
|
@ -8005,6 +8077,22 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
|
|
@ -9227,6 +9315,7 @@
|
|||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
|
@ -9694,6 +9783,22 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
|
|
@ -10509,6 +10614,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-retry-allowed": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
|
||||
"integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
|
|
@ -16247,6 +16365,13 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@iframe-resizer/child": "^5.3.3",
|
||||
"axios": "^0.23.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"core-js": "^3.38.1",
|
||||
"http-status-codes": "^2.3.0",
|
||||
|
|
@ -42,6 +41,9 @@
|
|||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^27.0.0",
|
||||
"axios": "^1.8.4",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"dotenv-safe": "^9.1.0",
|
||||
"eslint": "8.57",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
|
|
|
|||
111
playwright-tests/business-logic/types/JiraApi.ts
Normal file
111
playwright-tests/business-logic/types/JiraApi.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
export interface PutEditIssueRequestBody {
|
||||
fields: Partial<JiraIssueFields>
|
||||
}
|
||||
|
||||
export interface PostTransitionIssueRequestBody {
|
||||
transition: { id: string }
|
||||
}
|
||||
|
||||
export interface PostCreateIssueRequestBody extends JiraIssue {}
|
||||
|
||||
export interface PostCreateIssueResponse {
|
||||
id: string,
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface PostBulkCreateIssueRequestBody {
|
||||
issueUpdates: PostCreateIssueRequestBody[]
|
||||
}
|
||||
|
||||
export interface BulkTransitionInput {
|
||||
selectedIssueIdsOrKeys: string[],
|
||||
transitionId: string
|
||||
}
|
||||
|
||||
export interface PostBulkTransitionIssuesRequestBody {
|
||||
bulkTransitionInputs: BulkTransitionInput[],
|
||||
sendBulkNotification: false
|
||||
}
|
||||
|
||||
export interface PostBulkCreateIssueResponse {
|
||||
issues: PostCreateIssueResponse[]
|
||||
}
|
||||
|
||||
export interface PostAddCommentResponse {
|
||||
body: JiraContent
|
||||
}
|
||||
|
||||
export interface JiraIssueFields {
|
||||
summary: string,
|
||||
description: JiraContent,
|
||||
project?: JiraProject,
|
||||
issuetype?: { id: string },
|
||||
parent?: JiraParent,
|
||||
fixVersions?: JiraVersion[],
|
||||
subtasks?: JiraSubTask[],
|
||||
customfield_14857?: JiraContent // Test Steps field
|
||||
}
|
||||
|
||||
export interface JiraSubTask {
|
||||
id: string,
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface JiraProject {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface JiraParent {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface JiraContent {
|
||||
type?: string,
|
||||
text?: string,
|
||||
version?: number
|
||||
content?: JiraContent[]
|
||||
}
|
||||
|
||||
export interface JiraVersion {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface JiraTransition {
|
||||
id: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface JiraIssueType {
|
||||
id: string,
|
||||
self: string,
|
||||
description: string,
|
||||
iconUrl: string,
|
||||
name: string,
|
||||
untranslatedName: string,
|
||||
subtask: boolean,
|
||||
}
|
||||
|
||||
export interface GetIssueTransitionsResponse {
|
||||
transitions: JiraTransition[]
|
||||
}
|
||||
|
||||
export interface GetIssueTypesResponse {
|
||||
issueTypes: JiraIssueType[]
|
||||
}
|
||||
|
||||
export interface PostBulkFetchIssuesRequestBody {
|
||||
issueIdsOrKeys: string[]
|
||||
}
|
||||
|
||||
export interface PostBulkFetchIssuesResponse {
|
||||
issues: GetIssueResponse[];
|
||||
}
|
||||
|
||||
export interface JiraIssue {
|
||||
key?: string,
|
||||
id?: string,
|
||||
transition?: { id: string },
|
||||
fields: JiraIssueFields
|
||||
}
|
||||
|
||||
export interface GetIssueResponse extends JiraIssue {}
|
||||
143
playwright-tests/impl/API/JiraApiUtil.ts
Normal file
143
playwright-tests/impl/API/JiraApiUtil.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody } from "@business-logic/types/JiraApi";
|
||||
import axios, { request } from "axios";
|
||||
import axiosRetry from "axios-retry";
|
||||
|
||||
const jiraUrl = process.env.JIRA_SERVER!;
|
||||
const jiraUsername = process.env.JIRA_USERNAME!;
|
||||
const jiraApiKey = process.env.JIRA_API_KEY!;
|
||||
const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64');
|
||||
|
||||
export default class JiraApiUtil {
|
||||
readonly baseUrl: string;
|
||||
readonly issueUrl: string;
|
||||
readonly bulkIssueCreateUrl: string;
|
||||
readonly bulkIssueFetchUrl: string;
|
||||
readonly bulkTransitionIssuesUrl: string;
|
||||
readonly axiosClient: axios.AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = jiraUrl;
|
||||
this.issueUrl = `${this.baseUrl}/rest/api/3/issue`;
|
||||
this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`;
|
||||
this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`;
|
||||
this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition`
|
||||
this.axiosClient = axios.create();
|
||||
// interceptor to log error message from api
|
||||
this.axiosClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('Axios Error:', error?.response?.data || error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// Set up retries if requests are made too quickly
|
||||
axiosRetry(this.axiosClient, {
|
||||
retries: 4,
|
||||
retryDelay: (retryCount) => { return Math.pow(2, retryCount) * 1000; }, // Exponential backoff
|
||||
retryCondition: (error) => { return error.response?.status === 429 } // If rate-limit error
|
||||
});
|
||||
}
|
||||
|
||||
getIssue(issueKey: string) {
|
||||
const url = `${this.issueUrl}/${issueKey}`;
|
||||
return this.axiosClient.get<GetIssueResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) {
|
||||
return this.axiosClient.post<PostBulkFetchIssuesResponse>(this.bulkIssueFetchUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIssueTransitions(issueKey: string) {
|
||||
const url = `${this.issueUrl}/${issueKey}/transitions`
|
||||
return this.axiosClient.get<GetIssueTransitionsResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIssueTypes(projectKey: string) {
|
||||
const url = `${this.baseUrl}/rest/api/3/issue/createmeta/${projectKey}/issuetypes`;
|
||||
return this.axiosClient.get<GetIssueTypesResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postAddComment(issueKey: string, commentBody: PostAddCommentResponse) {
|
||||
const url = `${this.issueUrl}/${issueKey}/comment`;
|
||||
return this.axiosClient.post(url, commentBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postTransitionIssue(issueKey: string, requestBody: PostTransitionIssueRequestBody) {
|
||||
const transitionUrl = `${this.issueUrl}/${issueKey}/transitions`
|
||||
return this.axiosClient.post(transitionUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
putEditIssue(issueKey: string, requestBody: PutEditIssueRequestBody) {
|
||||
const editIssueUrl = `${this.issueUrl}/${issueKey}`
|
||||
return this.axiosClient.put(editIssueUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postBulkTransitionIssues(requestBody: PostBulkTransitionIssuesRequestBody) {
|
||||
return this.axiosClient.post(this.bulkTransitionIssuesUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
postCreateIssue(requestBody: PostCreateIssueRequestBody) {
|
||||
return this.axiosClient.post<PostCreateIssueResponse>(this.issueUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) {
|
||||
return this.axiosClient.post<PostBulkCreateIssueResponse>(this.bulkIssueCreateUrl, requestBody, {
|
||||
headers: {
|
||||
Authorization: `Basic ${encodedAuthKey}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
339
playwright-tests/impl/reporter/JiraWritebackReporter.ts
Normal file
339
playwright-tests/impl/reporter/JiraWritebackReporter.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import JiraApiUtil from "@impl/API/JiraApiUtil";
|
||||
import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
||||
import { formatDate } from "@impl/utils/DateUtils";
|
||||
import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi";
|
||||
|
||||
const jiraCardNumber = process.env.JIRA_CARD_NUMBER!;
|
||||
const jiraProjectKey = 'INSR';
|
||||
const passTransitionId = '111';
|
||||
const failTransitionId = '101';
|
||||
|
||||
export default class JiraWritebackReporter implements Reporter {
|
||||
readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil();
|
||||
readonly loadIssueCalls: (() => void)[] = []; // Calls to loadIssue must be deferred because they depend on information from the API.
|
||||
readonly issuesToPass: string[] = []; // Issues to transition to "Pass".
|
||||
readonly issuesToFail: string[] = []; // Issues to transition to "Fail".
|
||||
readonly issuesToCreate: JiraIssue[] = []; // Issues to be batch created.
|
||||
existingSubtasks: JiraIssue[] = []; // Array to hold existing subtasks of the dev card.
|
||||
testCaseTypeId: string|undefined = undefined; // ID of the test case subtask type in Jira. Will be filled by API call.
|
||||
bugTypeId:string|undefined = undefined; // ID of the bug subtask type.
|
||||
parentCard: GetIssueResponse | undefined = undefined; // Variable to hold the parent card. Will be filled by API call.
|
||||
|
||||
// Will need this if we want to move ortoni report upload into this reporter.
|
||||
// readonly ortoniReport = new OrtoniReport(reportConfig);
|
||||
|
||||
/**
|
||||
* This function loads the IDs for the Jira Issue Types we use.
|
||||
*
|
||||
*/
|
||||
async loadJiraIssueTypes() {
|
||||
console.log(`JiraWritebackReporter >> Loading Jira Issue Types for project '${jiraProjectKey}'...`);
|
||||
const issueTypesRes = await this.jiraApiUtil.getIssueTypes(jiraProjectKey);
|
||||
const issueTypes = issueTypesRes.data;
|
||||
this.testCaseTypeId = issueTypes.issueTypes.find(issueType => {
|
||||
return issueType.subtask === true && issueType.name === 'Test Case Sub-task'
|
||||
})?.id;
|
||||
this.bugTypeId = issueTypes.issueTypes.find(issueType => {
|
||||
return issueType.subtask === true && issueType.name === 'Bug Sub-task'
|
||||
})?.id;
|
||||
console.log('JiraWritebackReporter >> Loaded issue types.');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function generates a test subtask based on the test results we pass in.
|
||||
* @param test TestCase from onTestEnd()
|
||||
* @param result TestResult from onTestEnd()
|
||||
* @returns Jira Test Subtask based on test result.
|
||||
*/
|
||||
getCreateTestSubtask(test: TestCase, result: TestResult) {
|
||||
if (result.status === 'skipped') {
|
||||
return undefined;
|
||||
}
|
||||
const testStepTitles = result.steps.map(step => {
|
||||
return `-\t${step.title}`;
|
||||
}).join('\n');
|
||||
const allErrors = result.errors.map(value => {
|
||||
return value.message
|
||||
}).join('\n');
|
||||
|
||||
const description = `Most recent test status: ${result.status}.\nDuration: ${result.duration/1000} seconds.\nErrors:\n${allErrors}`
|
||||
const testSubtaskBody: JiraIssueFields = {
|
||||
summary: test.title,
|
||||
description: {
|
||||
content: [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": description,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
},
|
||||
project: {
|
||||
key: jiraProjectKey
|
||||
},
|
||||
issuetype: {
|
||||
id: this.testCaseTypeId!
|
||||
},
|
||||
parent: {
|
||||
key: jiraCardNumber
|
||||
},
|
||||
customfield_14857: {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: testStepTitles
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
return testSubtaskBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function generates a bug subtask based on the test results we pass in.
|
||||
* @param test TestCase from onTestEnd()
|
||||
* @param result TestResult from onTestEnd()
|
||||
* @returns Jira Bug Subtask based on test result.
|
||||
*/
|
||||
getCreateBug(test: TestCase, result: TestResult) {
|
||||
if (result.status === 'passed' || result.status === 'skipped') {
|
||||
return undefined;
|
||||
}
|
||||
const allErrors = result.errors.map(value => {
|
||||
return value.message
|
||||
}).join('\n');
|
||||
|
||||
const bug: JiraIssueFields = {
|
||||
summary: `TEST FAILED ${formatDate(new Date())}: ${test.title}`,
|
||||
description: {
|
||||
content: [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": allErrors,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
},
|
||||
project: {
|
||||
key: jiraProjectKey
|
||||
},
|
||||
issuetype: {
|
||||
id: this.bugTypeId!
|
||||
},
|
||||
parent: {
|
||||
key: jiraCardNumber
|
||||
}
|
||||
};
|
||||
return bug;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function gives us the correct transition for a test subtask based on this test result.
|
||||
* @param result TestResult from onTestEnd()
|
||||
* @returns Correct transition to pass to the Jira API.
|
||||
*/
|
||||
getSubtaskTransition(result: TestResult): { id: string } | undefined {
|
||||
if (result.status === 'skipped') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (result.status === 'passed') {
|
||||
return {
|
||||
id: passTransitionId
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: failTransitionId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs parent card and existing subtasks.
|
||||
*/
|
||||
async loadReporterData() {
|
||||
await this.loadJiraIssueTypes();
|
||||
|
||||
console.log('JiraWritebackReporter >> Searching for parent card...');
|
||||
const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber);
|
||||
const parentCard = parentRes.data;
|
||||
console.log(`JiraWritebackReporter >> Retrieved parent card: ${parentCard.key}: ${parentCard.fields.summary}`);
|
||||
|
||||
console.log(`JiraWritebackReporter >> Searching for all subtasks of parent card '${jiraCardNumber}'...`);
|
||||
const subtaskIds = parentCard.fields.subtasks?.map(subtask => {
|
||||
return subtask.id;
|
||||
});
|
||||
|
||||
if (subtaskIds && subtaskIds.length > 0) {
|
||||
for (let i = 0; i < subtaskIds.length; i+=50) {
|
||||
const batch = subtaskIds.slice(i, i+50);
|
||||
const subTasksRes = await this.jiraApiUtil.postBulkFetchIssues({ issueIdsOrKeys: batch });
|
||||
this.existingSubtasks.push(...subTasksRes.data.issues);
|
||||
}
|
||||
console.log('JiraWritebackReporter >> Subtasks retrieved.');
|
||||
} else {
|
||||
console.log('JiraWritebackReporter >> No subtasks were found.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk creates test cases and bugs in batches of up to 50. Bulk transitions existing test case subtasks.
|
||||
*/
|
||||
async writeResults() {
|
||||
for (let i = 0; i < this.issuesToCreate.length; i+=50) {
|
||||
const batch = this.issuesToCreate.slice(i, i + 50);
|
||||
console.log('JiraWritebackReporter >> Creating issue batch...');
|
||||
// fire request
|
||||
await this.jiraApiUtil.postBulkCreateIssue({ issueUpdates: batch });
|
||||
console.log('JiraWritebackReporter >> Issue batch created.');
|
||||
}
|
||||
|
||||
if (this.issuesToPass.length > 0) {
|
||||
console.log('JiraWritebackReporter >> Transitioning passed test cases to "Pass"...')
|
||||
await this.jiraApiUtil.postBulkTransitionIssues( {
|
||||
bulkTransitionInputs: [{
|
||||
selectedIssueIdsOrKeys: this.issuesToPass,
|
||||
transitionId: passTransitionId
|
||||
}],
|
||||
sendBulkNotification: false
|
||||
});
|
||||
console.log('JiraWritebackReporter >> Transition success.');
|
||||
}
|
||||
|
||||
if (this.issuesToFail.length > 0) {
|
||||
console.log('JiraWritebackReporter >> Transitioning failed test cases to "Fail"...')
|
||||
await this.jiraApiUtil.postBulkTransitionIssues( {
|
||||
bulkTransitionInputs: [{
|
||||
selectedIssueIdsOrKeys: this.issuesToFail,
|
||||
transitionId: failTransitionId
|
||||
}],
|
||||
sendBulkNotification: false
|
||||
});
|
||||
console.log('JiraWritebackReporter >> Transition success.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and loads appropriate response based on test result. This can be a test subtask/bug subtask or a call to transition a test subtask.
|
||||
* @param test TestCase from onTestEnd()
|
||||
* @param result TestResult from onTestEnd()
|
||||
* @returns Void
|
||||
*/
|
||||
loadIssue(test: TestCase, result: TestResult) {
|
||||
if (result.status === 'skipped') return;
|
||||
if (result.status !== 'passed' && result.retry < test.retries) return; // Skip if test case failed and this isn't the last retry.
|
||||
|
||||
let existingTestSubtask: JiraIssue | undefined = undefined;
|
||||
let existingBug: JiraIssue | undefined = undefined;
|
||||
const subtaskTransition = this.getSubtaskTransition(result)!; // Get either a Pass or Fail transition depending on test results.
|
||||
|
||||
console.log('JiraWritebackReporter >> Checking for existing subtasks for this test...');
|
||||
for (const card of this.existingSubtasks) {
|
||||
if (card.fields.issuetype?.id === this.testCaseTypeId) {
|
||||
if (card.fields.summary === test.title) {
|
||||
existingTestSubtask = card;
|
||||
console.log('JiraWritebackReporter >> Found existing test subtask.');
|
||||
}
|
||||
} else if (card.fields.issuetype?.id === this.bugTypeId) {
|
||||
if (new RegExp(`^TEST FAILED [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]: ${test.title}$`).test(card.fields.summary)) {
|
||||
existingBug = card;
|
||||
console.log('JiraWritebackReporter >> Found existing bug.');
|
||||
// TODO: Handle multiple matches for bug/test case. Currently picks one in that case.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!(existingTestSubtask || existingBug)) {
|
||||
console.log('JiraWritebackReporter >> No existing test subtask or bug was found.')
|
||||
}
|
||||
|
||||
const subTask = this.getCreateTestSubtask(test, result); // Will return undefined if test status is 'skipped'
|
||||
const bug = this.getCreateBug(test, result); // Will return undefined if we don't need one
|
||||
if (subTask) {
|
||||
// Create or Edit Subtask
|
||||
if (existingTestSubtask) {
|
||||
if (result.status === 'passed') {
|
||||
this.issuesToPass.push(existingTestSubtask.key!);
|
||||
} else {
|
||||
this.issuesToFail.push(existingTestSubtask.key!);
|
||||
}
|
||||
} else {
|
||||
this.issuesToCreate.push({
|
||||
fields: subTask,
|
||||
transition: subtaskTransition
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (bug) {
|
||||
// Create or Edit Bug
|
||||
if (existingBug) {
|
||||
// Leave it alone. This reporter should not modify existing bugs.
|
||||
} else {
|
||||
this.issuesToCreate.push({
|
||||
fields: bug
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads calls to loadIssue() in an array to be executed when the necessary information from the Jira API is available.
|
||||
* @param test
|
||||
* @param result
|
||||
*/
|
||||
onTestEnd(test: TestCase, result: TestResult) {
|
||||
// this.ortoniReport.onTestEnd(test, result);
|
||||
this.loadIssueCalls.push(() => { this.loadIssue(test, result); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes calls to load and execute API calls to Jira.
|
||||
* @param result
|
||||
* @returns Promise to write results to Jira.
|
||||
*/
|
||||
onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void {
|
||||
// this.ortoniReport.onEnd(result);
|
||||
return this.loadReporterData().then(async () => {
|
||||
this.loadIssueCalls.map(fn => fn());
|
||||
await this.writeResults();
|
||||
console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`);
|
||||
});
|
||||
}
|
||||
|
||||
// These overloads will become necessary if we move the ortoni report upload into this reporter
|
||||
// onBegin(config: FullConfig, suite: Suite): void {
|
||||
// this.ortoniReport.onBegin(config, suite);
|
||||
// }
|
||||
|
||||
// onError(error: TestError): void {
|
||||
// this.ortoniReport.onError(error);
|
||||
// }
|
||||
|
||||
// onExit(): Promise<void> {
|
||||
// return this.ortoniReport.onExit();
|
||||
// }
|
||||
|
||||
// onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
|
||||
// this.ortoniReport.onStdOut(chunk, test, result);
|
||||
// }
|
||||
}
|
||||
Loading…
Reference in a new issue