Updates the report configuration to adjust folder paths, logo locations, and the project name, ensuring correct paths and the proper project is referenced in reports.
449 lines
No EOL
18 KiB
TypeScript
449 lines
No EOL
18 KiB
TypeScript
import JiraApiUtil from "@impl/API/JiraApiUtil";
|
|
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from "@playwright/test/reporter";
|
|
import { formatDate, formatDateForFilename } from "@impl/utils/DateUtils";
|
|
import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi";
|
|
import OrtoniReport, { OrtoniReportConfig } from "ortoni-report";
|
|
|
|
let jiraCardNumber = process.env.JIRA_CARD_NUMBER || '';
|
|
const isRegressionRun = process.env.IS_REGRESSION === 'true'? true: false;
|
|
const jiraProjectKey = process.env.JIRA_PROJECT_KEY!;
|
|
const jiraEpicKey = process.env.JIRA_EPIC_KEY!;
|
|
const passTransitionId = '111';
|
|
const failTransitionId = '101';
|
|
const inTestTransitionId = '271';
|
|
const currentDate = formatDateForFilename(new Date());
|
|
|
|
// Ortoni config
|
|
const reportName = `ortoni_report_${currentDate}.html`;
|
|
const reportConfig: OrtoniReportConfig = {
|
|
port: 1994,
|
|
open: "never",
|
|
folderPath: "ortoni-report",
|
|
filename: reportName,
|
|
logo: 'playwright-tests/business-logic/data/logo.png',
|
|
title: "Test Report",
|
|
showProject: false,
|
|
projectName: "ISS-Nextgen-Playwright-Report",
|
|
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
|
|
preferredTheme: "light",
|
|
base64Image: true,
|
|
};
|
|
|
|
export default class JiraWritebackReporter implements Reporter {
|
|
readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil();
|
|
readonly ortoniReport: OrtoniReport = new OrtoniReport(reportConfig);
|
|
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.
|
|
userStoryTypeId:string|undefined = undefined;
|
|
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;
|
|
this.userStoryTypeId = issueTypes.issueTypes.find(issueType => {
|
|
return issueType.name === 'Story';
|
|
})?.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
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param cardName
|
|
* @param sprintId
|
|
* @returns
|
|
*/
|
|
getCreateRegressionCardRequestBody(cardName: string, sprintId: number) {
|
|
const body: JiraIssueFields = {
|
|
summary: cardName,
|
|
description: {
|
|
content: [
|
|
{
|
|
"content": [
|
|
{
|
|
"text": 'Card to hold regression test results for this sprint.',
|
|
"type": "text"
|
|
}
|
|
],
|
|
"type": "paragraph"
|
|
}
|
|
],
|
|
"type": "doc",
|
|
"version": 1
|
|
},
|
|
project: {
|
|
key: jiraProjectKey
|
|
},
|
|
issuetype: { id: this.userStoryTypeId!},
|
|
parent: { key: jiraEpicKey },
|
|
customfield_10007: sprintId,
|
|
customfield_13100: { id: '557058:a314fc5b-aed9-4472-90f8-00f106e06207' } // Mark UAT Tester as N/A
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* Grabs parent card and existing subtasks.
|
|
*/
|
|
async loadReporterData() {
|
|
let parentCard: GetIssueResponse;
|
|
await this.loadJiraIssueTypes();
|
|
if (isRegressionRun) {
|
|
console.log('JiraWritebackReporter >> Searching for current regression card...');
|
|
const sprint = await this.jiraApiUtil.getCurrentSprint();
|
|
if (!sprint) {
|
|
console.error('JiraWritebackReporter >> Could not find current sprint');
|
|
} else {
|
|
console.log(`JiraWritebackReporter >> Found sprint with id '${sprint.id}' called '${sprint.name}'`);
|
|
}
|
|
const regressionCardName = `Playwright Automated Regression Tests ${sprint?.name}`;
|
|
const parentRes = await this.jiraApiUtil.getJqlSearchIssue({ jql: `sprint = ${sprint?.id} and summary ~ "${regressionCardName}"` });
|
|
if (parentRes.data.issues.length > 1) {
|
|
console.log('JiraWritebackReporter >> WARNING: Multiple regression cards found. Using first one.');
|
|
parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
|
|
await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
|
|
transition: {
|
|
id: inTestTransitionId
|
|
}
|
|
});
|
|
} else if (parentRes.data.issues.length < 1) {
|
|
// NO regression card found. Make one.
|
|
console.log('JiraWritebackReporter >> No regression card was found for this sprint. Creating one.');
|
|
const fields = this.getCreateRegressionCardRequestBody(regressionCardName, sprint!.id);
|
|
const regressionCard: JiraIssue = {
|
|
fields: fields,
|
|
transition: {
|
|
id: inTestTransitionId
|
|
}
|
|
};
|
|
const createRes = await this.jiraApiUtil.postCreateIssue(regressionCard);
|
|
regressionCard.id = createRes.data.id;
|
|
regressionCard.key = createRes.data.key;
|
|
parentCard = regressionCard;
|
|
} else {
|
|
// Found one
|
|
console.log('JiraWritebackReporter >> Found one regression card. Using it.');
|
|
parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
|
|
await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
|
|
transition: {
|
|
id: inTestTransitionId
|
|
}
|
|
});
|
|
}
|
|
jiraCardNumber = parentCard.key!;
|
|
console.log(`JiraWritebackReporter >> Retrieved current regression card: ${parentCard.key}: ${parentCard.fields.summary}`);
|
|
} else {
|
|
console.log('JiraWritebackReporter >> Searching for parent card...');
|
|
const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber);
|
|
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.');
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
return this.ortoniReport.onEnd(result).then(async () => {
|
|
await this.loadReporterData().then(async () => {
|
|
this.loadIssueCalls.map(fn => fn());
|
|
await this.writeResults();
|
|
}).then(async () => {
|
|
console.log('JiraWritebackReporter >> Uploading Ortoni HTML Report to Jira...');
|
|
await this.jiraApiUtil.postUploadAttachment(jiraCardNumber, `${reportConfig.folderPath}/${reportConfig.filename}`);
|
|
console.log('JiraWritebackReporter >> Uploaded Ortoni HTML Report to Jira.');
|
|
console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`);
|
|
});
|
|
});
|
|
}
|
|
|
|
onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
return this.ortoniReport.onBegin(config, suite);
|
|
}
|
|
|
|
onError(error: TestError): void {
|
|
return this.ortoniReport.onError(error);
|
|
}
|
|
|
|
onExit(): Promise<void> {
|
|
return this.ortoniReport.onExit();
|
|
}
|
|
|
|
onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
|
|
return this.ortoniReport.onStdOut(chunk, test, result);
|
|
}
|
|
} |