cypress-xray-plugin
Version:
A Cypress plugin for uploading test results to Xray (test management for Jira)
322 lines (321 loc) • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RunConverterLatest = exports.RunConverterV12 = void 0;
const node_path_1 = require("node:path");
const util_1 = require("../../../../util");
const status_conversion_1 = require("./status-conversion");
/**
* Converts Cypress test results for Cypress versions <13.
*/
class RunConverterV12 {
/**
* Constructs a new converter for the specified run results.
*
* @param projectKey - the project key
* @param runResults - the run results
*/
constructor(projectKey, runResults) {
this.projectKey = projectKey;
this.runResults = runResults;
}
getConversions(options) {
const conversions = [];
for (const run of this.runResults) {
for (const test of run.tests) {
const title = test.title.join(" ");
const attempts = options.onlyLastAttempt
? [test.attempts[test.attempts.length - 1]]
: test.attempts;
let issueKeys;
try {
issueKeys = (0, util_1.getTestIssueKeys)(title, this.projectKey);
}
catch (_a) {
issueKeys = [null];
}
for (const attempt of attempts) {
for (const issueKey of issueKeys) {
try {
conversions.push({
duration: attempt.duration,
issueKey,
kind: "success",
spec: { filepath: run.spec.absolute },
startedAt: new Date(attempt.startedAt),
status: (0, status_conversion_1.toCypressStatus)(attempt.state),
title: title,
});
}
catch (error) {
conversions.push({ error, kind: "error", title });
}
}
}
}
}
return conversions;
}
getNonAttributableScreenshots() {
return [];
}
getScreenshots(issueKey, options) {
const screenshots = [];
for (const run of this.runResults) {
for (const test of run.tests) {
const title = test.title.join(" ");
const attempts = options.onlyLastAttempt
? [test.attempts[test.attempts.length - 1]]
: test.attempts;
try {
const testTitleKeys = (0, util_1.getTestIssueKeys)(title, this.projectKey);
if (testTitleKeys.includes(issueKey)) {
for (const attempt of attempts) {
for (const screenshot of attempt.screenshots) {
screenshots.push(screenshot.path);
}
}
}
}
catch (_a) {
continue;
}
}
}
return [...new Set([...screenshots])];
}
}
exports.RunConverterV12 = RunConverterV12;
/**
* Converts Cypress test results for Cypress versions ≥13.
*/
class RunConverterLatest {
/**
* Constructs a new converter for the specified run results.
*
* @param projectKey - the project key
* @param runResults - the run results
* @param screenshotDetails - all screenshots taken during the run
*/
constructor(projectKey, runResults, screenshotDetails) {
this.projectKey = projectKey;
this.runResults = runResults;
this.screenshotDetails = screenshotDetails;
}
getConversions(options) {
const conversions = [];
const testStarts = {};
for (const run of this.runResults) {
const startTime = new Date(run.stats.startedAt).getTime();
let totalDuration = 0;
for (let i = 0; i < run.tests.length; i++) {
let date;
if (i === 0) {
date = new Date(startTime);
}
else {
date = new Date(startTime + totalDuration);
}
totalDuration = totalDuration + run.tests[i].duration;
testStarts[run.tests[i].title.join(" ")] = date;
}
}
for (const run of this.runResults) {
for (const test of run.tests) {
const title = test.title.join(" ");
const attempts = options.onlyLastAttempt
? [test.attempts[test.attempts.length - 1]]
: test.attempts;
let issueKeys;
try {
issueKeys = (0, util_1.getTestIssueKeys)(title, this.projectKey);
}
catch (_a) {
issueKeys = [null];
}
for (const attempt of attempts) {
for (const issueKey of issueKeys) {
try {
conversions.push({
duration: test.duration,
issueKey,
kind: "success",
spec: { filepath: run.spec.absolute },
startedAt: testStarts[title],
status: (0, status_conversion_1.toCypressStatus)(attempt.state),
title: title,
});
}
catch (error) {
conversions.push({ error, kind: "error", title });
}
}
}
}
}
return conversions;
}
getNonAttributableScreenshots(options) {
let screenshots = this.screenshotDetails;
if (options.onlyLastAttempt) {
screenshots = this.filterLastAttemptScreenshots(screenshots);
}
screenshots = screenshots.filter((screenshot) => {
try {
return (0, util_1.getTestIssueKeys)(screenshot.path, this.projectKey).length === 0;
}
catch (_a) {
return true;
}
});
return [...new Set([...screenshots.map((screenshot) => screenshot.path)])];
}
getScreenshots(issueKey, options) {
let screenshots = [];
for (const screenshot of this.screenshotDetails) {
if (!screenshot.path.includes(issueKey)) {
continue;
}
screenshots.push(screenshot);
}
if (options.onlyLastAttempt) {
screenshots = this.filterLastAttemptScreenshots(screenshots);
}
return [...new Set([...screenshots.map((screenshot) => screenshot.path)])];
}
filterLastAttemptScreenshots(screenshots) {
// Group screenshots by their "basename", i.e. without the (attempt xxx) suffixes.
const groups = this.groupScreenshots(screenshots);
const lastScreenshots = [];
for (const similarScreenshots of groups) {
const screenshotsByAttemptIndex = new Map();
for (const screenshot of similarScreenshots) {
const match = RunConverterLatest.REGEX_ATTEMPT.exec(screenshot.path);
if (match !== null) {
const attemptIndex = Number.parseInt(match[1]);
const attemptScreenshots = screenshotsByAttemptIndex.get(attemptIndex);
if (attemptScreenshots) {
attemptScreenshots.push(screenshot);
}
else {
screenshotsByAttemptIndex.set(attemptIndex, [screenshot]);
}
}
}
// Only keep the latest attempts if present.
if (screenshotsByAttemptIndex.size > 0) {
const latestAttempts = [...screenshotsByAttemptIndex.entries()].reduce(([previousIndex, previousScreenshots], [currentIndex, currentScreenshots]) => currentIndex > previousIndex
? [currentIndex, currentScreenshots]
: [previousIndex, previousScreenshots], [Number.NEGATIVE_INFINITY, []]);
lastScreenshots.push(...latestAttempts[1]);
}
else {
lastScreenshots.push(...similarScreenshots);
}
}
// Remove all screenshots of failed attempts that have been superseded by a passed one
// without screenshot.
const tests = this.runResults.flatMap((result) => result.tests);
return lastScreenshots.filter((screenshot) => {
if (!screenshot.testFailure) {
return true;
}
const sanitizedScreenshotName = this.sanitizeName(screenshot.path);
return tests.some((test) => {
const sanitizedTestTitle = this.sanitizeName(test.title[test.title.length - 1]);
let finalAttemptName;
if (test.attempts.length > 1) {
finalAttemptName = `${sanitizedTestTitle} (${test.state}) (attempt ${test.attempts.length.toString()})${(0, node_path_1.extname)(screenshot.path)}`;
}
else {
finalAttemptName = `${sanitizedTestTitle} (${test.state})${(0, node_path_1.extname)(screenshot.path)}`;
}
return sanitizedScreenshotName.endsWith(finalAttemptName);
});
});
}
/**
* Removes illegal filename characters from a string (based on Windows).
*
* Note: this mirrors what Cypress is doing internally when computing file paths.
*
* @param s - the input string
* @returns the sanitized string
*
* @see https://stackoverflow.com/a/42210346
* @see https://github.com/cypress-io/cypress/blob/667e3196381c7e7b4b09a00c1b3f42d70a3f944b/packages/server/lib/screenshots.ts#L417-L421
*/
sanitizeName(s) {
// eslint-disable-next-line no-control-regex
return s.replaceAll(/[/\\?%*:|"<>\u0000-\u001F]/g, "");
}
/**
* Groups screenshots by their basenames, i.e. without the `(attempt xxx)` or conflict suffixes.
*
* @param screenshots - the screenshots
* @returns the grouped screenshots
*/
groupScreenshots(screenshots) {
const screenshotGroups = [];
// Reverse order because we're popping (order is important for upload order later on).
let remainingScreenshots = [...screenshots].reverse();
while (remainingScreenshots.length > 0) {
// Cast valid: it cannot ever be undefined here.
const screenshot = remainingScreenshots.pop();
const group = [screenshot];
const name = (0, node_path_1.basename)(screenshot.path, (0, node_path_1.extname)(screenshot.path));
// Try to find screenshots with possibly conflicting names.
if (RunConverterLatest.REGEX_CONFLICT.exec(name) !== null) {
const nameWithoutSuffix = name.replace(RunConverterLatest.REGEX_CONFLICT, "");
for (let i = remainingScreenshots.length - 1; i >= 0; i--) {
const otherScreenshot = remainingScreenshots[i];
let otherName = (0, node_path_1.basename)(otherScreenshot.path, (0, node_path_1.extname)(otherScreenshot.path));
otherName = otherName.replace(RunConverterLatest.REGEX_CONFLICT, "");
if (otherName === nameWithoutSuffix) {
group.push(otherScreenshot);
}
}
remainingScreenshots = remainingScreenshots.filter((s1) => group.every((s2) => s1.path !== s2.path));
}
// Try to find screenshots of similar attempts.
if (RunConverterLatest.REGEX_ATTEMPT.exec(name) !== null) {
const nameWithoutSuffix = name.replace(RunConverterLatest.REGEX_ATTEMPT, "");
for (let i = remainingScreenshots.length - 1; i >= 0; i--) {
const otherScreenshot = remainingScreenshots[i];
const otherName = (0, node_path_1.basename)(otherScreenshot.path, (0, node_path_1.extname)(otherScreenshot.path));
if (otherName === nameWithoutSuffix) {
group.push(otherScreenshot);
}
}
}
else {
for (let i = remainingScreenshots.length - 1; i >= 0; i--) {
const otherScreenshot = remainingScreenshots[i];
let otherName = (0, node_path_1.basename)(otherScreenshot.path, (0, node_path_1.extname)(otherScreenshot.path));
otherName = otherName.replace(RunConverterLatest.REGEX_ATTEMPT, "");
if (otherName === name) {
group.push(otherScreenshot);
}
}
}
remainingScreenshots = remainingScreenshots.filter((s1) => group.every((s2) => s1.path !== s2.path));
screenshotGroups.push(group);
}
return screenshotGroups;
}
}
exports.RunConverterLatest = RunConverterLatest;
/**
* - Initial run: `CYP-123 my screenshot.png`
* - Retry #1: `CYP-123 my screenshot (attempt 2).png`
* - Retry #2: `CYP-123 my screenshot (attempt 3).png`
*
* @see https://docs.cypress.io/app/guides/test-retries#Screenshots
*/
RunConverterLatest.REGEX_ATTEMPT = /\s+\(attempt (\d+)\)/;
/**
* - `CYP-123 my screenshot (1).png`
* - `CYP-123 my screenshot (2).png`
*
* @see https://github.com/cypress-io/cypress/blob/667e3196381c7e7b4b09a00c1b3f42d70a3f944b/packages/server/lib/screenshots.ts#L365
*/
RunConverterLatest.REGEX_CONFLICT = /\s+(\d+)$/;