@gurglosa/playwright-zephyr-jira-reporter
Version:
A custom Playwright reporter that syncs test results to Zephyr Scale and creates Jira bugs on failures.
236 lines (235 loc) • 11.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const ZephyrServices_1 = require("./ZephyrServices");
const JiraServices_1 = require("./JiraServices");
class ZephyrJiraReporter {
constructor(config) {
this.zephyrService = null; // Service for interacting with Zephyr Scale
this.jiraService = null; // Service for interacting with Jira
this.testResults = new Map();
// Initialize configuration with environment variables as fallback
try {
this.config = {
Zephyr_Base_URL: config.Zephyr_Base_URL,
Zephyr_Access_Token: config.Zephyr_Access_Token,
Zephyr_Test_Cycle_ID: config.Zephyr_Test_Cycle_ID,
Zephyr_Test_Project_Key: config.Zephyr_Test_Project_Key,
Zephyr_Enabled: config.Zephyr_Enabled,
Jira_Access_Token: config.Jira_Access_Token,
Jira_Base_URL: config.Jira_Base_URL,
Jira_Email: config.Jira_Email,
Jira_project_Key: config.Jira_project_Key,
Jira_Enabled: config.Jira_Enabled,
};
// Initialize the Zephyr Scale service if reporting is enabled
if (this.config.Zephyr_Enabled) {
this.zephyrService = new ZephyrServices_1.ZephyrServices({
Zephyr_Base_URL: this.config.Zephyr_Base_URL,
Zephyr_Access_Token: this.config.Zephyr_Access_Token,
Zephyr_Test_Cycle_ID: this.config.Zephyr_Test_Cycle_ID,
Zephyr_Test_Project_Key: this.config.Zephyr_Test_Project_Key,
Zephyr_Enabled: this.config.Zephyr_Enabled,
});
}
// Initialize the Jira service if reporting is enabled
if (this.config.Jira_Enabled) {
this.jiraService = new JiraServices_1.JiraServices({
Jira_Base_URL: this.config.Jira_Base_URL,
Jira_Access_Token: this.config.Jira_Access_Token,
Jira_Email: this.config.Jira_Email,
Jira_project_Key: this.config.Jira_project_Key,
Jira_Enabled: this.config.Jira_Enabled,
});
}
}
catch (error) {
console.error('Error initializing ZephyrJiraReporter configuration or services:', error);
throw new Error('Failed to initialize ZephyrJiraReporter. Please check the provided configuration.');
}
}
onTestEnd(testCase, testResult) {
// if (!this.config.Zephyr_Enabled) return;
const { testCaseKey, testCycleKey } = this.extractKeys(testCase.title);
if (!testCaseKey) {
console.log(`No Jira test case key found in test title: ${testCase.title}`);
return;
}
// Use test cycle key from the title or fall back to the default
const cycleKey = testCycleKey || this.config.Zephyr_Test_Cycle_ID;
if (!cycleKey) {
console.log(`No test cycle key found for test: ${testCase.title}`);
return;
}
const status = this.mapTestStatus(testResult.status);
let errorMessage = '';
if (testResult.error) {
errorMessage = testResult.error.stack || String(testResult.error); // Capture error details if present
}
// Extract test script details from the test result
const testScript = this.extractTestScript(testResult);
// Store the test result for later reporting
this.testResults.set(testCaseKey, {
title: testCase.title,
status,
error: errorMessage,
testCycleKey: cycleKey,
testScript,
duration: testResult.duration,
});
}
async onEnd() {
// console.log(this.config.Jira_Access_Token)
// if (!this.config.Zephyr_Enabled || !this.zephyrService || this.testResults.size === 0) return;
console.log(`\nUpdating ${this.testResults.size} test results in Zephyr Scale...`);
const updatePromises = Array.from(this.testResults.entries()).map(async ([testCaseKey, result]) => {
try {
if (this.config.Zephyr_Enabled && this.zephyrService) {
await this.zephyrService.updateTestExecutionStatus({
projectKey: this.config.Zephyr_Test_Project_Key,
testCaseKey,
testCycleKey: result.testCycleKey,
statusName: result.status,
testScriptResults: result.testScript,
environmentName: 'TEST',
actualEndDate: new Date().toISOString(),
executedBy: 'Automated Test Runner',
assignedToId: '712020:40b4e2a2-b204-427e-867a-041cd0510010',
executionTime: result.duration,
comment: result.error ? `Test failed with error: ${result.error}` : undefined,
});
}
if (this.config.Jira_Enabled && this.jiraService) {
await this.handleJiraIssue(testCaseKey, result);
}
}
catch (error) {
if (error instanceof Error) {
console.log(`Failed to update test case ${testCaseKey} in Zephyr Scale:`, error.message);
}
else {
console.log(`Failed to update test case ${testCaseKey} in Zephyr Scale:`, String(error));
}
}
});
await Promise.all(updatePromises);
console.log('Zephyr Scale update and Jira migration completed');
}
async handleJiraIssue(testCaseKey, result) {
if (!this.jiraService)
return;
try {
const searchResult = await this.jiraService.searchIssueWithTitleUsingJQL(`${testCaseKey}`);
const existingIssues = searchResult.sections[0]?.issues || [];
if (existingIssues.length === 1) {
const issue = existingIssues[0];
console.log(`⚠️ Jira issue already exists for ${testCaseKey}: ${issue.key}`);
const currentStatus = (await this.jiraService.getJiraIssue(issue.key)).fields.status.name;
console.log(`📌 Current Jira issue status: ${currentStatus}`);
const transitionMap = {
[ZephyrServices_1.TestExecutionStatus.failed]: ['Ready For Testing', 'testing'].includes(currentStatus)
? JiraServices_1.IssueStatusTransition.in_progress : undefined,
[ZephyrServices_1.TestExecutionStatus.passed]: JiraServices_1.IssueStatusTransition.done
};
const status = result.status;
const transition = transitionMap[status];
if (currentStatus === "Done" || !transition) {
if (result.status === ZephyrServices_1.TestExecutionStatus.failed) {
await this.createNewJiraIssue(testCaseKey, result);
console.log(`❌ Test case ${testCaseKey} failed, Jira issue is already in "Done" status. Creating a new issue. check it manually on duplication.`);
}
console.log(`🔄 ⚠️ No transition needed for "${currentStatus}"`);
}
else {
await this.jiraService.transitionIssue(issue.key, transition);
console.log(`✅ Transitioned ${issue.key} to ${this.getTransitionName(transition)}`);
}
}
else if (result.status === ZephyrServices_1.TestExecutionStatus.failed && existingIssues.length === 0) {
await this.createNewJiraIssue(testCaseKey, result);
}
else if (existingIssues.length > 1) {
console.log(`⚠️ Multiple Jira issues found for ${testCaseKey}. Please check manually.`);
}
}
catch (error) {
console.error(`❌ Jira issue handling failed for test ${testCaseKey}`, error);
}
}
;
async createNewJiraIssue(testCaseKey, result) {
let stepNum = 1;
const descriptionText = `Failed: ${result.title}\n` +
result.testScript?.map((step) => `Step ${stepNum++}: ${step.actualResult} - Status: ${step.statusName}`).join('\n');
const issue = await this.jiraService.createIssue({
fields: {
project: { key: this.config.Jira_project_Key },
summary: result.title,
customfield_10069: testCaseKey,
description: {
type: 'doc',
version: 1,
content: [{ type: 'paragraph', content: [{ type: 'text', text: descriptionText }] }],
},
issuetype: { id: '10002' },
reporter: { id: '6113c0ba9798100070110305' }
}
});
if (this.config.Zephyr_Enabled && this.zephyrService) {
await this.zephyrService?.linkIssueToTestCase(testCaseKey, {
issueId: issue.id
});
}
console.log(`✅ Jira issue created: ${issue.key} and linked to test case ${testCaseKey}`);
}
;
getTransitionName(value) {
return Object.entries(JiraServices_1.IssueStatusTransition).find(([_, val]) => val === value)?.[0];
}
;
extractTestScript(result) {
try {
// Filter out steps categorized as 'hook' (e.g., setup/teardown steps)
const filteredSteps = result.steps.filter(step => step.category !== 'hook');
// Map each step to an object containing its execution status and result
const stepScriptWithErrors = filteredSteps.map(step => ({
actualResult: step.error
? `Step "${step.title}" failed with error: ${this.stripAnsiCodes(step.error.stack || 'Unknown error')}`
: "Step executed successfully",
statusName: step.error ? 'fail' : 'pass'
}));
return stepScriptWithErrors;
}
catch (error) {
console.error('Failed to extract test script:', error);
return undefined; // Return undefined if extraction fails
}
}
stripAnsiCodes(str) {
return str.replace(/\x1B\[\d+m/g, ''); // Removes ANSI color codes
}
extractKeys(title) {
const testCaseMatch = title.match(/\[([A-Z]+-T\d+)\]/); // Matches test case keys like [MYA-T123]
const testCycleMatch = title.match(/\[([A-Z]+-R\d+)\]/); // Matches test cycle keys like [MYA-R9]
return {
testCaseKey: testCaseMatch ? testCaseMatch[1] : null,
testCycleKey: testCycleMatch ? testCycleMatch[1] : null
};
}
mapTestStatus(status) {
switch (status) {
case 'passed':
return ZephyrServices_1.TestExecutionStatus.passed;
case 'failed':
return ZephyrServices_1.TestExecutionStatus.failed;
case 'skipped':
return ZephyrServices_1.TestExecutionStatus.skipped;
case 'timedOut':
return ZephyrServices_1.TestExecutionStatus.timedOut;
case 'interrupted':
return ZephyrServices_1.TestExecutionStatus.interrupted;
default:
return ZephyrServices_1.TestExecutionStatus.UNKNOWN;
}
}
}
exports.default = ZephyrJiraReporter;