UNPKG

cypress-xray-plugin

Version:

A Cypress plugin for uploading test results to Xray (test management for Jira)

322 lines (321 loc) 14.1 kB
"use strict"; 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 &lt;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 &ge;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+)$/;