From 18c4f6bc02a5d799541f5d24eb791e14759321b2 Mon Sep 17 00:00:00 2001 From: Basti <18561435+csvtuda@users.noreply.github.com> Date: Sun, 27 Oct 2024 02:59:12 +0200 Subject: [PATCH] Bump cypress-xray-plugin from 7.3.0 to 7.4.0 (#390) * Cleanup * Cleanup * Prefer issue data over deprecated options * Clean up * Bump cypress-xray-plugin from 7.3.0 to 7.3.1 * Fix ESLint issue * Add issue transitioning * Add explicit issue transitioning * Move towards single issue update retrieval * Add after run builder * Fix test issues * Refactor issue data handling * Rename variable * Fix Cypress tests * Fix Cucumber tests * Fix tests * Fix test * Bump cypress-xray-plugin from 7.3.1 to 7.4.0 * Fix test plan handling * Update `CHANGELOG.md` --- .eslintrc.json | 1 + CHANGELOG.md | 24 + package-lock.json | 61 +- package.json | 2 +- src/client/jira/jira-client.spec.ts | 74 ++ src/client/jira/jira-client.ts | 40 +- src/context.spec.ts | 44 - src/context.ts | 19 +- src/env.ts | 3 +- src/hooks/after/after-run.spec.ts | 361 +++--- src/hooks/after/after-run.ts | 1123 ++++++++++------- .../conversion/convert-info-command.spec.ts | 144 +-- .../conversion/convert-info-command.ts | 96 +- .../convert-cucumber-features-command.spec.ts | 210 +-- .../convert-cucumber-features-command.ts | 20 +- .../convert-cypress-tests-command.spec.ts | 238 +++- .../cypress/convert-cypress-tests-command.ts | 38 +- .../conversion/util/multipart-info.spec.ts | 80 +- .../conversion/util/multipart-info.ts | 67 +- ...tract-execution-issue-type-command.spec.ts | 331 ----- .../extract-execution-issue-type-command.ts | 114 -- .../preprocessor/file-preprocessor.spec.ts | 5 +- src/hooks/preprocessor/file-preprocessor.ts | 7 +- .../jira/edit-issue-field-command.spec.ts | 7 +- .../commands/jira/edit-issue-field-command.ts | 18 +- .../jira/extract-field-id-command.spec.ts | 128 +- .../commands/jira/extract-field-id-command.ts | 12 - .../commands/jira/get-field-values-command.ts | 17 +- .../jira/get-label-values-command.spec.ts | 32 +- .../commands/jira/get-label-values-command.ts | 19 +- .../jira/get-summary-values-command.ts | 3 +- .../jira/get-test-type-values-command.spec.ts | 196 --- .../jira/get-test-type-values-command.ts | 57 - .../jira/transition-issue-command.spec.ts | 32 + .../commands/jira/transition-issue-command.ts | 33 + src/plugin.spec.ts | 2 - src/types/plugin.ts | 92 +- src/types/util.ts | 10 +- src/util/compare.spec.ts | 101 ++ src/util/compare.ts | 15 +- src/util/functions.ts | 11 +- src/util/graph/logging/graph-logger.ts | 5 +- src/util/help.ts | 2 - test/mocks.ts | 3 + 44 files changed, 1856 insertions(+), 2041 deletions(-) delete mode 100644 src/hooks/after/commands/extract-execution-issue-type-command.spec.ts delete mode 100644 src/hooks/after/commands/extract-execution-issue-type-command.ts delete mode 100644 src/hooks/util/commands/jira/get-test-type-values-command.spec.ts delete mode 100644 src/hooks/util/commands/jira/get-test-type-values-command.ts create mode 100644 src/hooks/util/commands/jira/transition-issue-command.spec.ts create mode 100644 src/hooks/util/commands/jira/transition-issue-command.ts create mode 100644 src/util/compare.spec.ts diff --git a/.eslintrc.json b/.eslintrc.json index c88760d0..60e83199 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,7 @@ }, "rules": { "no-shadow": "off", + "@typescript-eslint/no-deprecated": "off", "@typescript-eslint/parameter-properties": "error", "@typescript-eslint/non-nullable-type-assertion-style": "off", "@typescript-eslint/member-ordering": "error", diff --git a/CHANGELOG.md b/CHANGELOG.md index a30166ae..7ab160e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +# `7.4.0` + +## Notable changes + +- Removed unused `jira.fields.testType` + +- Projects other than the configured project key can now be used for the test execution issue key + +- Added explicit issue transition call for server environments ([#389](https://github.com/Qytera-Gmbh/cypress-xray-plugin/pull/389)) + +- Added Cypress test results parameter to test execution issue callback + +- Added Cypress test results parameter to test plan key callback + +## Dependency updates + +- Bump @bahmutov/cypress-esbuild-preprocessor from 2.2.2 to 2.2.3 + +- Bump axios from 1.7.5 to 1.7.7 + +- Bump @badeball/cypress-cucumber-preprocessor from 20.1.1 to 21.0.0 + +- Bump cypress from 13.13.2 to 13.14.1 + # `7.3.0` ## Notable changes diff --git a/package-lock.json b/package-lock.json index 785fd1aa..2d9b32b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cypress-xray-plugin", - "version": "7.3.0", + "version": "7.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cypress-xray-plugin", - "version": "7.3.0", + "version": "7.4.0", "license": "MIT", "dependencies": { "@cucumber/gherkin": "^28.0.0", @@ -2330,23 +2330,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz", - "integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.15.0", - "@typescript-eslint/visitor-keys": "7.15.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", @@ -2487,7 +2470,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz", "integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==", - "devOptional": true, + "optional": true, "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -2500,7 +2483,7 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz", "integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==", - "devOptional": true, + "optional": true, "dependencies": { "@typescript-eslint/types": "7.15.0", "@typescript-eslint/visitor-keys": "7.15.0", @@ -2528,7 +2511,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "devOptional": true, + "optional": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2539,33 +2522,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz", - "integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.15.0", - "@typescript-eslint/types": "7.15.0", - "@typescript-eslint/typescript-estree": "7.15.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz", "integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==", - "devOptional": true, + "optional": true, "dependencies": { "@typescript-eslint/types": "7.15.0", "eslint-visitor-keys": "^3.4.3" @@ -2826,8 +2787,8 @@ }, "node_modules/array-union": { "version": "2.1.0", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } @@ -4023,8 +3984,8 @@ }, "node_modules/dir-glob": { "version": "3.0.1", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "path-type": "^4.0.0" }, @@ -5500,8 +5461,8 @@ }, "node_modules/globby": { "version": "11.1.0", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -7918,8 +7879,8 @@ }, "node_modules/path-type": { "version": "4.0.0", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } @@ -8957,8 +8918,8 @@ }, "node_modules/slash": { "version": "3.0.0", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } diff --git a/package.json b/package.json index 9d3882bd..b22b854d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress-xray-plugin", - "version": "7.3.0", + "version": "7.4.0", "description": "A Cypress plugin for uploading test results to Xray (test management for Jira)", "types": "index.d.ts", "author": "csvtuda", diff --git a/src/client/jira/jira-client.spec.ts b/src/client/jira/jira-client.spec.ts index 9bed1c35..c6d30f97 100644 --- a/src/client/jira/jira-client.spec.ts +++ b/src/client/jira/jira-client.spec.ts @@ -609,5 +609,79 @@ describe(path.relative(process.cwd(), __filename), () => { ); }); }); + + describe("transitionIssue", () => { + it("transitions issues", async () => { + restClient.post.onFirstCall().resolves({ + config: { + headers: new AxiosHeaders(), + }, + data: undefined, + headers: {}, + status: HttpStatusCode.NoContent, + statusText: HttpStatusCode[HttpStatusCode.NoContent], + }); + await client.transitionIssue("CYP-XYZ", { + transition: { + name: "resolve", + to: { + name: "done", + }, + }, + }); + expect(restClient.post).to.have.been.calledOnceWith( + "https://example.org/rest/api/latest/issue/CYP-XYZ/transitions", + { + transition: { + name: "resolve", + to: { + name: "done", + }, + }, + }, + { + headers: { ["Authorization"]: "Basic dXNlcjp0b2tlbg==" }, + } + ); + }); + + it("handles bad responses", async () => { + const logger = getMockedLogger({ allowUnstubbedCalls: true }); + const error = new AxiosError( + "Request failed with status code 404", + HttpStatusCode.NotFound.toString(), + undefined, + null, + { + config: { headers: new AxiosHeaders() }, + data: { + errorMessages: ["issue CYP-XYZ does not exist"], + }, + headers: {}, + status: HttpStatusCode.NotFound, + statusText: HttpStatusCode[HttpStatusCode.NotFound], + } + ); + restClient.post.onFirstCall().rejects(error); + await expect( + client.transitionIssue("CYP-XYZ", { + transition: { + name: "resolve", + to: { + name: "done", + }, + }, + }) + ).to.eventually.be.rejectedWith("Failed to transition issue"); + expect(logger.message).to.have.been.calledWithExactly( + Level.ERROR, + "Failed to transition issue: Request failed with status code 404" + ); + expect(logger.logErrorToFile).to.have.been.calledOnceWithExactly( + error, + "transitionIssue" + ); + }); + }); }); }); diff --git a/src/client/jira/jira-client.ts b/src/client/jira/jira-client.ts index 7feb2ed4..e382cef9 100644 --- a/src/client/jira/jira-client.ts +++ b/src/client/jira/jira-client.ts @@ -83,6 +83,21 @@ export interface JiraClient { * @see https://docs.atlassian.com/software/jira/docs/api/REST/9.9.1/#api/2/search-searchUsingSearchRequest */ search(request: SearchRequest): Promise; + /** + * Performs an issue transition and, if the transition has a screen, updates the fields from the + * transition screen. + * + * To update the fields on the transition screen, specify the fields in the `fields` or `update` + * parameters in the request body. Get details about the fields using + * [Get transitions](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-transitions-get) + * with the `transitions.fields` expand. + * + * @param issueIdOrKey - the ID or key of the issue + * @param issueUpdateData - the issue update data + * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-transitions-post + * @see https://docs.atlassian.com/software/jira/docs/api/REST/9.9.1/#api/2/issue-doTransition + */ + transitionIssue(issueIdOrKey: string, issueUpdateData: IssueUpdate): Promise; } /** @@ -298,7 +313,6 @@ export class BaseJiraClient extends Client implements JiraClient { } ); LOG.message(Level.DEBUG, `Successfully edited issue: ${issueIdOrKey}`); - return issueIdOrKey; } catch (error: unknown) { LOG.message(Level.ERROR, `Failed to edit issue: ${errorMessage(error)}`); @@ -306,4 +320,28 @@ export class BaseJiraClient extends Client implements JiraClient { throw new LoggedError("Failed to edit issue"); } } + + public async transitionIssue( + issueIdOrKey: string, + issueUpdateData: IssueUpdate + ): Promise { + try { + const header = await this.credentials.getAuthorizationHeader(); + LOG.message(Level.DEBUG, "Transitioning issue..."); + await this.httpClient.post( + `${this.apiBaseUrl}/rest/api/latest/issue/${issueIdOrKey}/transitions`, + issueUpdateData, + { + headers: { + ...header, + }, + } + ); + LOG.message(Level.DEBUG, `Successfully transitioned issue: ${issueIdOrKey}`); + } catch (error: unknown) { + LOG.message(Level.ERROR, `Failed to transition issue: ${errorMessage(error)}`); + LOG.logErrorToFile(error, "transitionIssue"); + throw new LoggedError("Failed to transition issue"); + } + } } diff --git a/src/context.spec.ts b/src/context.spec.ts index 9f9dfac5..48094993 100644 --- a/src/context.spec.ts +++ b/src/context.spec.ts @@ -73,9 +73,6 @@ describe(path.relative(process.cwd(), __filename), () => { it("testPlan", () => { expect(jiraOptions.fields.testPlan).to.eq(undefined); }); - it("testType", () => { - expect(jiraOptions.fields.testType).to.eq(undefined); - }); }); it("testExecutionIssue", () => { expect(jiraOptions.testExecutionIssue).to.eq(undefined); @@ -273,19 +270,6 @@ describe(path.relative(process.cwd(), __filename), () => { ); expect(jiraOptions.fields.testPlan).to.eq("Plan de Test"); }); - it("testType", () => { - const jiraOptions = initJiraOptions( - {}, - { - fields: { - testType: "Xray Test Type", - }, - projectKey: "PRJ", - url: "https://example.org", - } - ); - expect(jiraOptions.fields.testType).to.eq("Xray Test Type"); - }); }); it("testExecutionIssue", () => { const jiraOptions = initJiraOptions( @@ -730,20 +714,6 @@ describe(path.relative(process.cwd(), __filename), () => { }); expect(jiraOptions.fields.testPlan).to.eq("customfield_98765"); }); - - it("JIRA_FIELDS_TEST_TYPE", () => { - const env = { - ["JIRA_FIELDS_TEST_TYPE"]: "customfield_98765", - }; - const jiraOptions = initJiraOptions(env, { - fields: { - testType: "customfield_12345", - }, - projectKey: "PRJ", - url: "https://example.org", - }); - expect(jiraOptions.fields.testType).to.eq("customfield_98765"); - }); }); it("JIRA_TEST_EXECUTION_ISSUE", () => { @@ -1153,20 +1123,6 @@ describe(path.relative(process.cwd(), __filename), () => { ) ).to.throw("Plugin misconfiguration: Jira project key was not set"); }); - it("detect mismatched test execution issue keys", () => { - expect(() => - initJiraOptions( - {}, - { - projectKey: "CYP", - testExecutionIssueKey: "ABC-123", - url: "https://example.org", - } - ) - ).to.throw( - "Plugin misconfiguration: test execution issue key ABC-123 does not belong to project CYP" - ); - }); it("throws if the cucumber preprocessor is not installed", async () => { stub(dependencies, "IMPORT").rejects(new Error("Failed to import package")); await expect( diff --git a/src/context.ts b/src/context.ts index f3cd03b9..0191e39a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -138,15 +138,7 @@ export function initJiraOptions( if (!projectKey) { throw new Error("Plugin misconfiguration: Jira project key was not set"); } - const testExecutionIssueKey = - parse(env, ENV_NAMES.jira.testExecutionIssueKey, asString) ?? options.testExecutionIssueKey; - if (testExecutionIssueKey && !testExecutionIssueKey.startsWith(projectKey)) { - throw new Error( - `Plugin misconfiguration: test execution issue key ${testExecutionIssueKey} does not belong to project ${projectKey}` - ); - } - const testPlanIssueKey = - parse(env, ENV_NAMES.jira.testPlanIssueKey, asString) ?? options.testPlanIssueKey; + return { attachVideos: parse(env, ENV_NAMES.jira.attachVideos, asBoolean) ?? options.attachVideos ?? false, @@ -161,8 +153,6 @@ export function initJiraOptions( options.fields?.testEnvironments, testPlan: parse(env, ENV_NAMES.jira.fields.testPlan, asString) ?? options.fields?.testPlan, - testType: - parse(env, ENV_NAMES.jira.fields.testType, asString) ?? options.fields?.testType, }, projectKey: projectKey, testExecutionIssue: @@ -170,7 +160,9 @@ export function initJiraOptions( testExecutionIssueDescription: parse(env, ENV_NAMES.jira.testExecutionIssueDescription, asString) ?? options.testExecutionIssueDescription, - testExecutionIssueKey: testExecutionIssueKey, + testExecutionIssueKey: + parse(env, ENV_NAMES.jira.testExecutionIssueKey, asString) ?? + options.testExecutionIssueKey, testExecutionIssueSummary: parse(env, ENV_NAMES.jira.testExecutionIssueSummary, asString) ?? options.testExecutionIssueSummary, @@ -178,7 +170,8 @@ export function initJiraOptions( parse(env, ENV_NAMES.jira.testExecutionIssueType, asString) ?? options.testExecutionIssueType ?? "Test Execution", - testPlanIssueKey: testPlanIssueKey, + testPlanIssueKey: + parse(env, ENV_NAMES.jira.testPlanIssueKey, asString) ?? options.testPlanIssueKey, testPlanIssueType: parse(env, ENV_NAMES.jira.testPlanIssueType, asString) ?? options.testPlanIssueType ?? diff --git a/src/env.ts b/src/env.ts index 5f64a457..5cff9e59 100644 --- a/src/env.ts +++ b/src/env.ts @@ -25,7 +25,7 @@ interface Authentication { export const ENV_NAMES: Remap< Omit & Authentication, string, - "testExecutionIssue" + ["testExecutionIssue"] > = { authentication: { jira: { @@ -55,7 +55,6 @@ export const ENV_NAMES: Remap< summary: "JIRA_FIELDS_SUMMARY", testEnvironments: "JIRA_FIELDS_TEST_ENVIRONMENTS", testPlan: "JIRA_FIELDS_TEST_PLAN", - testType: "JIRA_FIELDS_TEST_TYPE", }, projectKey: "JIRA_PROJECT_KEY", testExecutionIssue: "JIRA_TEST_EXECUTION_ISSUE", diff --git a/src/hooks/after/after-run.spec.ts b/src/hooks/after/after-run.spec.ts index c2f160b5..7e13d38f 100644 --- a/src/hooks/after/after-run.spec.ts +++ b/src/hooks/after/after-run.spec.ts @@ -31,8 +31,8 @@ import { FallbackCommand } from "../util/commands/fallback-command"; import { AttachFilesCommand } from "../util/commands/jira/attach-files-command"; import { ExtractFieldIdCommand, JiraField } from "../util/commands/jira/extract-field-id-command"; import { FetchAllFieldsCommand } from "../util/commands/jira/fetch-all-fields-command"; -import { FetchIssueTypesCommand } from "../util/commands/jira/fetch-issue-types-command"; import { GetSummaryValuesCommand } from "../util/commands/jira/get-summary-values-command"; +import { TransitionIssueCommand } from "../util/commands/jira/transition-issue-command"; import { ImportExecutionCucumberCommand } from "../util/commands/xray/import-execution-cucumber-command"; import { ImportExecutionCypressCommand } from "../util/commands/xray/import-execution-cypress-command"; import { ImportFeatureCommand } from "../util/commands/xray/import-feature-command"; @@ -47,14 +47,10 @@ import { ConvertCucumberFeaturesCommand } from "./commands/conversion/cucumber/c import { AssertCypressConversionValidCommand } from "./commands/conversion/cypress/assert-cypress-conversion-valid-command"; import { CombineCypressJsonCommand } from "./commands/conversion/cypress/combine-cypress-xray-command"; import { ConvertCypressTestsCommand } from "./commands/conversion/cypress/convert-cypress-tests-command"; -import { ExtractExecutionIssueTypeCommand } from "./commands/extract-execution-issue-type-command"; import { ExtractVideoFilesCommand } from "./commands/extract-video-files-command"; import { VerifyExecutionIssueKeyCommand } from "./commands/verify-execution-issue-key-command"; import { VerifyResultsUploadCommand } from "./commands/verify-results-upload-command"; -// REMOVE IN VERSION 8.0.0 -/* eslint-disable @typescript-eslint/no-deprecated */ - chai.use(chaiAsPromised); describe(path.relative(process.cwd(), __filename), () => { @@ -123,8 +119,8 @@ describe(path.relative(process.cwd(), __filename), () => { const [ resultsCommand, convertCypressTestsCommand, - fetchIssueTypesCommand, - extractExecutionIssueTypeCommand, + issueSummaryCommand, + issuetypeCommand, convertCommand, combineCypressJsonCommand, assertCypressConversionValidCommand, @@ -134,11 +130,8 @@ describe(path.relative(process.cwd(), __filename), () => { ] = [...graph.getVertices()]; assertIsInstanceOf(resultsCommand, ConstantCommand); assertIsInstanceOf(convertCypressTestsCommand, ConvertCypressTestsCommand); - assertIsInstanceOf(fetchIssueTypesCommand, FetchIssueTypesCommand); - assertIsInstanceOf( - extractExecutionIssueTypeCommand, - ExtractExecutionIssueTypeCommand - ); + assertIsInstanceOf(issueSummaryCommand, ConstantCommand); + assertIsInstanceOf(issuetypeCommand, ConstantCommand); assertIsInstanceOf(convertCommand, ConvertInfoServerCommand); assertIsInstanceOf(combineCypressJsonCommand, CombineCypressJsonCommand); assertIsInstanceOf( @@ -151,17 +144,36 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertex data. expect(resultsCommand.getValue()).to.deep.eq(result); expect(convertCypressTestsCommand.getParameters()).to.deep.eq({ - cucumber: options.cucumber, evidenceCollection: new SimpleEvidenceCollection(), - jira: options.jira, - plugin: options.plugin, + featureFileExtension: ".feature", + normalizeScreenshotNames: false, + projectKey: "CYP", + uploadScreenshots: true, useCloudStatusFallback: false, - xray: options.xray, + xrayStatus: { + failed: undefined, + passed: undefined, + pending: undefined, + skipped: undefined, + step: { + failed: undefined, + passed: undefined, + pending: undefined, + skipped: undefined, + }, + }, }); expect(convertCommand.getParameters()).to.deep.eq({ - jira: options.jira, + jira: { + projectKey: options.jira.projectKey, + testPlanIssueKey: undefined, + }, xray: options.xray, }); + expect(issueSummaryCommand.getValue()).to.deep.eq( + "Execution Results [2022-11-28T17:41:12.234Z]" + ); + expect(issuetypeCommand.getValue()).to.deep.eq({ name: "Test Execution" }); expect(combineCypressJsonCommand.getParameters()).to.deep.eq({ testExecutionIssueKey: undefined, }); @@ -183,12 +195,7 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(convertCypressTestsCommand)]).to.deep.eq([ combineCypressJsonCommand, ]); - expect([...graph.getSuccessors(fetchIssueTypesCommand)]).to.deep.eq([ - extractExecutionIssueTypeCommand, - ]); - expect([...graph.getSuccessors(extractExecutionIssueTypeCommand)]).to.deep.eq([ - convertCommand, - ]); + expect([...graph.getSuccessors(issueSummaryCommand)]).to.deep.eq([convertCommand]); expect([...graph.getSuccessors(convertCommand)]).to.deep.eq([ combineCypressJsonCommand, ]); @@ -227,10 +234,10 @@ describe(path.relative(process.cwd(), __filename), () => { const issueKeysCommand = commands[2]; const getSummaryValuesCommand = commands[3]; const destructureCommand = commands[4]; - const convertCommand = commands[7]; - const importCypressExecutionCommand = commands[10]; - const verifyExecutionIssueKeyCommand = commands[11]; - const fallbackCypressUploadCommand = commands[12]; + const convertCommand = commands[6]; + const importCypressExecutionCommand = commands[9]; + const verifyExecutionIssueKeyCommand = commands[10]; + const fallbackCypressUploadCommand = commands[11]; assertIsInstanceOf(issueKeysCommand, ConstantCommand); assertIsInstanceOf(getSummaryValuesCommand, GetSummaryValuesCommand); assertIsInstanceOf(destructureCommand, DestructureCommand); @@ -265,8 +272,8 @@ describe(path.relative(process.cwd(), __filename), () => { verifyExecutionIssueKeyCommand, fallbackCypressUploadCommand, ]); - expect(graph.size("vertices")).to.eq(14); - expect(graph.size("edges")).to.eq(15); + expect(graph.size("vertices")).to.eq(13); + expect(graph.size("edges")).to.eq(14); }); it("uses configured test execution issue data with known fields", async () => { @@ -299,8 +306,8 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(issueKeysCommand)]).to.deep.eq([ getSummaryValuesCommand, ]); - expect(graph.size("vertices")).to.eq(14); - expect(graph.size("edges")).to.eq(15); + expect(graph.size("vertices")).to.eq(13); + expect(graph.size("edges")).to.eq(14); }); it("uses configured summaries", async () => { @@ -318,7 +325,7 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertices. const commands = [...graph.getVertices()]; const executionIssueSummaryCommand = commands[2]; - const convertCommand = commands[5]; + const convertCommand = commands[4]; assertIsInstanceOf(executionIssueSummaryCommand, ConstantCommand); assertIsInstanceOf(convertCommand, ConvertInfoServerCommand); // Vertex data. @@ -327,8 +334,8 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(executionIssueSummaryCommand)]).to.deep.eq([ convertCommand, ]); - expect(graph.size("vertices")).to.eq(11); - expect(graph.size("edges")).to.eq(12); + expect(graph.size("vertices")).to.eq(10); + expect(graph.size("edges")).to.eq(11); }); it("uses configured custom issue data", async () => { @@ -350,21 +357,15 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const textExecutionIssueDataCommand = commands[2]; - const convertCommand = commands[5]; - assertIsInstanceOf(textExecutionIssueDataCommand, ConstantCommand); - assertIsInstanceOf(convertCommand, ConvertInfoServerCommand); + const issueUpdateCommand = commands[2]; + assertIsInstanceOf(issueUpdateCommand, ConstantCommand); // Vertex data. - expect(textExecutionIssueDataCommand.getValue()).to.deep.eq({ + expect(issueUpdateCommand.getValue()).to.deep.eq({ fields: { assignee: "someone else", ["customfield_12345"]: "bonjour", }, }); - // Edges. - expect([...graph.getSuccessors(textExecutionIssueDataCommand)]).to.deep.eq([ - convertCommand, - ]); expect(graph.size("vertices")).to.eq(11); expect(graph.size("edges")).to.eq(12); }); @@ -406,6 +407,70 @@ describe(path.relative(process.cwd(), __filename), () => { expect(graph.size("vertices")).to.eq(12); expect(graph.size("edges")).to.eq(14); }); + + it("explicitly transitions issues in server environments", async () => { + const graph = new ExecutableGraph(); + options.jira.testExecutionIssue = { + transition: { + id: "6", + }, + }; + clients.kind = "server"; + await addUploadCommands( + result, + ".", + options, + clients, + new SimpleEvidenceCollection(), + graph, + getMockedLogger() + ); + // Vertices. + const commands = [...graph.getVertices()]; + const verifyResultsUploadCommand = commands[10]; + const transitionIssueCommand = commands[11]; + assertIsInstanceOf(transitionIssueCommand, TransitionIssueCommand); + // Vertex data. + expect(transitionIssueCommand.getParameters()).to.deep.eq({ + jiraClient: clients.jiraClient, + transition: { + id: "6", + }, + }); + // Edges. + expect([...graph.getSuccessors(verifyResultsUploadCommand)]).to.contain( + transitionIssueCommand + ); + expect(graph.size("vertices")).to.eq(12); + expect(graph.size("edges")).to.eq(13); + }); + + it("does not explicitly transition issues in cloud environments", async () => { + const graph = new ExecutableGraph(); + options.jira.testExecutionIssue = { + transition: { + id: "6", + }, + }; + clients.kind = "cloud"; + await addUploadCommands( + result, + ".", + options, + clients, + new SimpleEvidenceCollection(), + graph, + getMockedLogger() + ); + // Vertices. + const commands = [...graph.getVertices()]; + const verifyResultsUploadCommand = commands[10]; + assertIsInstanceOf(verifyResultsUploadCommand, VerifyResultsUploadCommand); + // Edges. + expect([...graph.getSuccessors(verifyResultsUploadCommand)]).to.deep.eq([]); + expect(graph.size("vertices")).to.eq(11); + expect(graph.size("edges")).to.eq(12); + }); }); describe("cucumber", () => { @@ -452,10 +517,10 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertices. const [ cypressResultsCommand, - cucumberResultsCommand, - fetchIssueTypesCommand, - extractExecutionIssueTypeCommand, + issueSummaryCommand, + issuetypeCommand, convertMultipartInfoCommand, + cucumberResultsCommand, convertCucumberFeaturesCommand, combineCucumberMultipartCommand, assertConversionValidCommand, @@ -465,11 +530,8 @@ describe(path.relative(process.cwd(), __filename), () => { ] = [...graph.getVertices()]; assertIsInstanceOf(cypressResultsCommand, ConstantCommand); assertIsInstanceOf(cucumberResultsCommand, ConstantCommand); - assertIsInstanceOf(fetchIssueTypesCommand, FetchIssueTypesCommand); - assertIsInstanceOf( - extractExecutionIssueTypeCommand, - ExtractExecutionIssueTypeCommand - ); + assertIsInstanceOf(issueSummaryCommand, ConstantCommand); + assertIsInstanceOf(issuetypeCommand, ConstantCommand); assertIsInstanceOf(convertMultipartInfoCommand, ConvertInfoServerCommand); assertIsInstanceOf( convertCucumberFeaturesCommand, @@ -492,16 +554,17 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertex data. expect(cypressResultsCommand.getValue()).to.deep.eq(cypressResult); expect(cucumberResultsCommand.getValue()).to.deep.eq(cucumberResult); - expect(fetchIssueTypesCommand.getParameters()).to.deep.eq({ - jiraClient: clients.jiraClient, - }); - expect(extractExecutionIssueTypeCommand.getParameters()).to.deep.eq({ - displayCloudHelp: false, - projectKey: "CYP", - testExecutionIssueType: { name: "Test Execution" }, + expect(issueSummaryCommand.getValue()).to.deep.eq( + "Execution Results [2023-07-23T21:26:15.539Z]" + ); + expect(issuetypeCommand.getValue()).to.deep.eq({ + name: "Test Execution", }); expect(convertMultipartInfoCommand.getParameters()).to.deep.eq({ - jira: options.jira, + jira: { + projectKey: options.jira.projectKey, + testPlanIssueKey: undefined, + }, xray: options.xray, }); expect(convertCucumberFeaturesCommand.getParameters()).to.deep.eq({ @@ -547,10 +610,10 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(cucumberResultsCommand)]).to.deep.eq([ convertCucumberFeaturesCommand, ]); - expect([...graph.getSuccessors(fetchIssueTypesCommand)]).to.deep.eq([ - extractExecutionIssueTypeCommand, + expect([...graph.getSuccessors(issueSummaryCommand)]).to.deep.eq([ + convertMultipartInfoCommand, ]); - expect([...graph.getSuccessors(extractExecutionIssueTypeCommand)]).to.deep.eq([ + expect([...graph.getSuccessors(issuetypeCommand)]).to.deep.eq([ convertMultipartInfoCommand, ]); expect([...graph.getSuccessors(convertMultipartInfoCommand)]).to.deep.eq([ @@ -590,9 +653,9 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const fetchAllFieldsCommand = commands[4]; - const testPlanIdCommand = commands[5]; - const convertCommand = commands[6]; + const fetchAllFieldsCommand = commands[0]; + const testPlanIdCommand = commands[1]; + const convertCommand = commands[5]; assertIsInstanceOf(fetchAllFieldsCommand, FetchAllFieldsCommand); assertIsInstanceOf(testPlanIdCommand, ExtractFieldIdCommand); // Vertex data. @@ -628,8 +691,8 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const testPlanIdCommand = commands[4]; - const convertCommand = commands[5]; + const testPlanIdCommand = commands[0]; + const convertCommand = commands[4]; assertIsInstanceOf(testPlanIdCommand, ConstantCommand); // Vertex data. expect(testPlanIdCommand.getValue()).to.eq("customfield_12345"); @@ -655,9 +718,9 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const fetchAllFieldsCommand = commands[4]; - const testEnvironmentsIdCommand = commands[5]; - const convertCommand = commands[6]; + const fetchAllFieldsCommand = commands[0]; + const testEnvironmentsIdCommand = commands[1]; + const convertCommand = commands[5]; assertIsInstanceOf(fetchAllFieldsCommand, FetchAllFieldsCommand); assertIsInstanceOf(testEnvironmentsIdCommand, ExtractFieldIdCommand); // Vertex data. @@ -693,8 +756,8 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const testEnvironmentsIdCommand = commands[4]; - const convertCommand = commands[5]; + const testEnvironmentsIdCommand = commands[0]; + const convertCommand = commands[4]; assertIsInstanceOf(testEnvironmentsIdCommand, ConstantCommand); // Vertex data. expect(testEnvironmentsIdCommand.getValue()).to.eq("customfield_67890"); @@ -721,10 +784,10 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const fetchAllFieldsCommand = commands[4]; - const testPlanIdCommand = commands[5]; - const testEnvironmentsIdCommand = commands[6]; - const convertCommand = commands[7]; + const fetchAllFieldsCommand = commands[0]; + const testPlanIdCommand = commands[1]; + const testEnvironmentsIdCommand = commands[2]; + const convertCommand = commands[6]; assertIsInstanceOf(fetchAllFieldsCommand, FetchAllFieldsCommand); assertIsInstanceOf(testPlanIdCommand, ExtractFieldIdCommand); assertIsInstanceOf(testEnvironmentsIdCommand, ExtractFieldIdCommand); @@ -760,9 +823,9 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const testPlanIdCommand = commands[4]; - const testEnvironmentsIdCommand = commands[5]; - const convertCommand = commands[6]; + const testPlanIdCommand = commands[0]; + const testEnvironmentsIdCommand = commands[1]; + const convertCommand = commands[5]; assertIsInstanceOf(testPlanIdCommand, ConstantCommand); assertIsInstanceOf(testEnvironmentsIdCommand, ConstantCommand); // Vertex data. @@ -798,21 +861,19 @@ describe(path.relative(process.cwd(), __filename), () => { ); // Vertices. const commands = [...graph.getVertices()]; - const extractExecutionIssueTypeCommand = commands[3]; - const convertCommand = commands[4]; + const issuetypeCommand = commands[2]; + const convertCommand = commands[3]; const convertCucumberFeaturesCommand = commands[5]; - assertIsInstanceOf( - extractExecutionIssueTypeCommand, - ExtractExecutionIssueTypeCommand - ); + assertIsInstanceOf(issuetypeCommand, ConstantCommand); assertIsInstanceOf(convertCommand, ConvertInfoCloudCommand); - expect(extractExecutionIssueTypeCommand.getParameters()).to.deep.eq({ - displayCloudHelp: true, - projectKey: "CYP", - testExecutionIssueType: { name: "Test Execution" }, + expect(issuetypeCommand.getValue()).to.deep.eq({ + name: "Test Execution", }); expect(convertCommand.getParameters()).to.deep.eq({ - jira: options.jira, + jira: { + projectKey: options.jira.projectKey, + testPlanIssueKey: undefined, + }, xray: options.xray, }); expect(convertCucumberFeaturesCommand.getParameters()).to.deep.eq({ @@ -863,14 +924,13 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertices. const [ cypressResultsCommand, - cucumberResultsCommand, - testExecutionIssueKeyCommand, issueKeysCommand, getSummaryValuesCommand, destructureCommand, - fetchIssueTypesCommand, - extractExecutionIssueTypeCommand, - convertCommand, + issuetypeCommand, + convertInfoCloudCommand, + cucumberResultsCommand, + testExecutionIssueKeyCommand, convertCucumberFeaturesCommand, combineCucumberMultipartCommand, assertConversionValidCommand, @@ -885,12 +945,8 @@ describe(path.relative(process.cwd(), __filename), () => { assertIsInstanceOf(issueKeysCommand, ConstantCommand); assertIsInstanceOf(getSummaryValuesCommand, GetSummaryValuesCommand); assertIsInstanceOf(destructureCommand, DestructureCommand); - assertIsInstanceOf(fetchIssueTypesCommand, FetchIssueTypesCommand); - assertIsInstanceOf( - extractExecutionIssueTypeCommand, - ExtractExecutionIssueTypeCommand - ); - assertIsInstanceOf(convertCommand, ConvertInfoCloudCommand); + assertIsInstanceOf(issuetypeCommand, ConstantCommand); + assertIsInstanceOf(convertInfoCloudCommand, ConvertInfoCloudCommand); assertIsInstanceOf( convertCucumberFeaturesCommand, ConvertCucumberFeaturesCommand @@ -917,21 +973,19 @@ describe(path.relative(process.cwd(), __filename), () => { expect(cypressResultsCommand.getValue()).to.deep.eq(cypressResult); expect(cucumberResultsCommand.getValue()).to.deep.eq(cucumberResult); expect(testExecutionIssueKeyCommand.getValue()).to.deep.eq("CYP-42"); - expect(fetchIssueTypesCommand.getParameters()).to.deep.eq({ - jiraClient: clients.jiraClient, - }); - expect(extractExecutionIssueTypeCommand.getParameters()).to.deep.eq({ - displayCloudHelp: true, - projectKey: "CYP", - testExecutionIssueType: { name: "Test Run" }, + expect(issuetypeCommand.getValue()).to.deep.eq({ + name: "Test Run", }); expect(issueKeysCommand.getValue()).to.deep.eq(["CYP-42"]); expect(getSummaryValuesCommand.getParameters()).to.deep.eq({ jiraClient: clients.jiraClient, }); expect(destructureCommand.getParameters()).to.deep.eq({ accessor: "CYP-42" }); - expect(convertCommand.getParameters()).to.deep.eq({ - jira: options.jira, + expect(convertInfoCloudCommand.getParameters()).to.deep.eq({ + jira: { + projectKey: options.jira.projectKey, + testPlanIssueKey: undefined, + }, xray: options.xray, }); expect(convertCucumberFeaturesCommand.getParameters()).to.deep.eq({ @@ -974,7 +1028,7 @@ describe(path.relative(process.cwd(), __filename), () => { }); // Edges. expect([...graph.getSuccessors(cypressResultsCommand)]).to.contain( - convertCommand + convertInfoCloudCommand ); expect([...graph.getSuccessors(cucumberResultsCommand)]).to.deep.eq([ convertCucumberFeaturesCommand, @@ -982,11 +1036,8 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(testExecutionIssueKeyCommand)]).to.deep.eq([ convertCucumberFeaturesCommand, ]); - expect([...graph.getSuccessors(fetchIssueTypesCommand)]).to.deep.eq([ - extractExecutionIssueTypeCommand, - ]); - expect([...graph.getSuccessors(extractExecutionIssueTypeCommand)]).to.deep.eq([ - convertCommand, + expect([...graph.getSuccessors(issuetypeCommand)]).to.deep.eq([ + convertInfoCloudCommand, ]); expect([...graph.getSuccessors(issueKeysCommand)]).to.deep.eq([ getSummaryValuesCommand, @@ -995,9 +1046,9 @@ describe(path.relative(process.cwd(), __filename), () => { destructureCommand, ]); expect([...graph.getSuccessors(destructureCommand)]).to.deep.eq([ - convertCommand, + convertInfoCloudCommand, ]); - expect([...graph.getSuccessors(convertCommand)]).to.deep.eq([ + expect([...graph.getSuccessors(convertInfoCloudCommand)]).to.deep.eq([ combineCucumberMultipartCommand, ]); expect([...graph.getSuccessors(convertCucumberFeaturesCommand)]).to.deep.eq([ @@ -1017,8 +1068,8 @@ describe(path.relative(process.cwd(), __filename), () => { expect([...graph.getSuccessors(fallbackCucumberUploadCommand)]).to.deep.eq([ verifyResultsUploadCommand, ]); - expect(graph.size("vertices")).to.eq(16); - expect(graph.size("edges")).to.eq(16); + expect(graph.size("vertices")).to.eq(15); + expect(graph.size("edges")).to.eq(15); }); }); @@ -1176,29 +1227,26 @@ describe(path.relative(process.cwd(), __filename), () => { const [ cypressResultsCommand, convertCypressTestsCommand, - fetchIssueTypesCommand, - extractExecutionIssueTypeCommand, - convertCommand, + issueSummaryCommand, + issuetypeCommand, + convertInfoServerCommand, combineCypressJsonCommand, assertCypressConversionValidCommand, importExecutionCypressCommand, cucumberResultsCommand, - fallbackCypressUploadCommand, convertCucumberFeaturesCommand, combineCucumberMultipartCommand, assertCucumberConversionValidCommand, importCucumberExecutionCommand, + fallbackCypressUploadCommand, fallbackCucumberUploadCommand, verifyResultsUploadCommand, ] = [...graph.getVertices()]; assertIsInstanceOf(cypressResultsCommand, ConstantCommand); assertIsInstanceOf(convertCypressTestsCommand, ConvertCypressTestsCommand); - assertIsInstanceOf(fetchIssueTypesCommand, FetchIssueTypesCommand); - assertIsInstanceOf( - extractExecutionIssueTypeCommand, - ExtractExecutionIssueTypeCommand - ); - assertIsInstanceOf(convertCommand, ConvertInfoServerCommand); + assertIsInstanceOf(issueSummaryCommand, ConstantCommand); + assertIsInstanceOf(issuetypeCommand, ConstantCommand); + assertIsInstanceOf(convertInfoServerCommand, ConvertInfoServerCommand); assertIsInstanceOf(combineCypressJsonCommand, CombineCypressJsonCommand); assertIsInstanceOf( assertCypressConversionValidCommand, @@ -1222,23 +1270,36 @@ describe(path.relative(process.cwd(), __filename), () => { // Vertex data. expect(cypressResultsCommand.getValue()).to.deep.eq(cypressResult); expect(convertCypressTestsCommand.getParameters()).to.deep.eq({ - cucumber: options.cucumber, evidenceCollection: new SimpleEvidenceCollection(), - jira: options.jira, - plugin: options.plugin, + featureFileExtension: ".feature", + normalizeScreenshotNames: false, + projectKey: "CYP", + uploadScreenshots: true, useCloudStatusFallback: false, - xray: options.xray, - }); - expect(fetchIssueTypesCommand.getParameters()).to.deep.eq({ - jiraClient: clients.jiraClient, + xrayStatus: { + failed: undefined, + passed: undefined, + pending: undefined, + skipped: undefined, + step: { + failed: undefined, + passed: undefined, + pending: undefined, + skipped: undefined, + }, + }, }); - expect(extractExecutionIssueTypeCommand.getParameters()).to.deep.eq({ - displayCloudHelp: false, - projectKey: "CYP", - testExecutionIssueType: { name: "Test Execution" }, + expect(issueSummaryCommand.getValue()).to.deep.eq( + "Execution Results [2023-07-23T21:26:15.539Z]" + ); + expect(issuetypeCommand.getValue()).to.deep.eq({ + name: "Test Execution", }); - expect(convertCommand.getParameters()).to.deep.eq({ - jira: options.jira, + expect(convertInfoServerCommand.getParameters()).to.deep.eq({ + jira: { + projectKey: options.jira.projectKey, + testPlanIssueKey: undefined, + }, xray: options.xray, }); expect(combineCypressJsonCommand.getParameters()).to.deep.eq({ @@ -1295,18 +1356,18 @@ describe(path.relative(process.cwd(), __filename), () => { // Cypress. expect([...graph.getSuccessors(cypressResultsCommand)]).to.deep.eq([ convertCypressTestsCommand, - convertCommand, + convertInfoServerCommand, ]); expect([...graph.getSuccessors(convertCypressTestsCommand)]).to.deep.eq([ combineCypressJsonCommand, ]); - expect([...graph.getSuccessors(fetchIssueTypesCommand)]).to.deep.eq([ - extractExecutionIssueTypeCommand, + expect([...graph.getSuccessors(issueSummaryCommand)]).to.deep.eq([ + convertInfoServerCommand, ]); - expect([...graph.getSuccessors(extractExecutionIssueTypeCommand)]).to.deep.eq([ - convertCommand, + expect([...graph.getSuccessors(issuetypeCommand)]).to.deep.eq([ + convertInfoServerCommand, ]); - expect([...graph.getSuccessors(convertCommand)]).to.deep.eq([ + expect([...graph.getSuccessors(convertInfoServerCommand)]).to.deep.eq([ combineCypressJsonCommand, combineCucumberMultipartCommand, ]); @@ -1318,6 +1379,7 @@ describe(path.relative(process.cwd(), __filename), () => { importExecutionCypressCommand, ]); expect([...graph.getSuccessors(importExecutionCypressCommand)]).to.deep.eq([ + convertCucumberFeaturesCommand, fallbackCypressUploadCommand, ]); // Cucumber. @@ -1325,7 +1387,6 @@ describe(path.relative(process.cwd(), __filename), () => { convertCucumberFeaturesCommand, ]); expect([...graph.getSuccessors(fallbackCypressUploadCommand)]).to.deep.eq([ - convertCucumberFeaturesCommand, verifyResultsUploadCommand, ]); expect([...graph.getSuccessors(convertCucumberFeaturesCommand)]).to.deep.eq([ diff --git a/src/hooks/after/after-run.ts b/src/hooks/after/after-run.ts index 10dab800..74a62829 100644 --- a/src/hooks/after/after-run.ts +++ b/src/hooks/after/after-run.ts @@ -1,11 +1,21 @@ import fs from "fs"; import path from "path"; +import { XrayClient } from "../../client/xray/xray-client"; import { EvidenceCollection } from "../../context"; import { CypressRunResultType } from "../../types/cypress/cypress"; -import { IssueUpdate } from "../../types/jira/responses/issue-update"; -import { ClientCombination, InternalCypressXrayPluginOptions } from "../../types/plugin"; -import { MaybeFunction } from "../../types/util"; -import { CucumberMultipartFeature } from "../../types/xray/requests/import-execution-cucumber-multipart"; +import { IssueTransition } from "../../types/jira/responses/issue-transition"; +import { IssueTypeDetails } from "../../types/jira/responses/issue-type-details"; +import { + ClientCombination, + InternalCypressXrayPluginOptions, + PluginIssueUpdate, +} from "../../types/plugin"; +import { XrayTest } from "../../types/xray/import-test-execution-results"; +import { + CucumberMultipart, + CucumberMultipartFeature, +} from "../../types/xray/requests/import-execution-cucumber-multipart"; +import { MultipartInfo } from "../../types/xray/requests/import-execution-multipart-info"; import { getOrCall } from "../../util/functions"; import { ExecutableGraph } from "../../util/graph/executable-graph"; import { Level, Logger } from "../../util/logging"; @@ -15,15 +25,14 @@ import { DestructureCommand } from "../util/commands/destructure-command"; import { FallbackCommand } from "../util/commands/fallback-command"; import { AttachFilesCommand } from "../util/commands/jira/attach-files-command"; import { JiraField } from "../util/commands/jira/extract-field-id-command"; -import { FetchIssueTypesCommand } from "../util/commands/jira/fetch-issue-types-command"; import { GetSummaryValuesCommand } from "../util/commands/jira/get-summary-values-command"; +import { TransitionIssueCommand } from "../util/commands/jira/transition-issue-command"; import { ImportExecutionCucumberCommand } from "../util/commands/xray/import-execution-cucumber-command"; import { ImportExecutionCypressCommand } from "../util/commands/xray/import-execution-cypress-command"; import { ImportFeatureCommand } from "../util/commands/xray/import-feature-command"; import { getOrCreateConstantCommand, getOrCreateExtractFieldIdCommand } from "../util/util"; import { ConvertInfoCloudCommand, - ConvertInfoCommand, ConvertInfoServerCommand, } from "./commands/conversion/convert-info-command"; import { AssertCucumberConversionValidCommand } from "./commands/conversion/cucumber/assert-cucumber-conversion-valid-command"; @@ -32,17 +41,13 @@ import { ConvertCucumberFeaturesCommand } from "./commands/conversion/cucumber/c import { AssertCypressConversionValidCommand } from "./commands/conversion/cypress/assert-cypress-conversion-valid-command"; import { CombineCypressJsonCommand } from "./commands/conversion/cypress/combine-cypress-xray-command"; import { ConvertCypressTestsCommand } from "./commands/conversion/cypress/convert-cypress-tests-command"; -import { ExtractExecutionIssueTypeCommand } from "./commands/extract-execution-issue-type-command"; import { ExtractVideoFilesCommand } from "./commands/extract-video-files-command"; import { VerifyExecutionIssueKeyCommand } from "./commands/verify-execution-issue-key-command"; import { VerifyResultsUploadCommand } from "./commands/verify-results-upload-command"; import { containsCucumberTest, containsCypressTest } from "./util"; -// REMOVE IN VERSION 8.0.0 -/* eslint-disable @typescript-eslint/no-deprecated */ - export async function addUploadCommands( - runResult: CypressRunResultType, + results: CypressRunResultType, projectRoot: string, options: InternalCypressXrayPluginOptions, clients: ClientCombination, @@ -51,11 +56,11 @@ export async function addUploadCommands( logger: Logger ) { const containsCypressTests = containsCypressTest( - runResult, + results, options.cucumber?.featureFileExtension ); const containsCucumberTests = containsCucumberTest( - runResult, + results, options.cucumber?.featureFileExtension ); if (!containsCypressTests && !containsCucumberTests) { @@ -65,69 +70,46 @@ export async function addUploadCommands( ); return; } - const cypressResultsCommand = getOrCreateConstantCommand(graph, logger, runResult); - let importCypressExecutionCommand: ImportExecutionCypressCommand | null = null; - let importCucumberExecutionCommand: ImportExecutionCucumberCommand | null = null; + const issueData = await getOrCall(options.jira.testExecutionIssue, { results }); + const testPlanIssueKey = await getOrCall(options.jira.testPlanIssueKey, { results }); + const builder = new AfterRunBuilder({ + clients: clients, + evidenceCollection: evidenceCollection, + graph: graph, + issueData: issueData, + logger: logger, + options: options, + results: results, + }); + let importCypressExecutionCommand; + let importCucumberExecutionCommand; if (containsCypressTests) { - importCypressExecutionCommand = await getImportExecutionCypressCommand( - cypressResultsCommand, - options, - clients, - evidenceCollection, - graph, - logger - ); + importCypressExecutionCommand = getImportExecutionCypressCommand(graph, clients, builder, { + reusesExecutionIssue: + issueData?.key !== undefined || options.jira.testExecutionIssueKey !== undefined, + testEnvironments: options.xray.testEnvironments, + testPlanIssueKey: testPlanIssueKey, + }); } if (containsCucumberTests) { - if (!options.cucumber?.preprocessor?.json.output) { - throw new Error( - "Failed to prepare Cucumber upload: Cucumber preprocessor JSON report path not configured." - ); - } - // Cypress might change process.cwd(), so we need to query the root directory. - // See: https://github.com/cypress-io/cypress/issues/22689 - const reportPath = path.resolve(projectRoot, options.cucumber.preprocessor.json.output); - const cucumberResults = JSON.parse( - fs.readFileSync(reportPath, "utf-8") - ) as CucumberMultipartFeature[]; - const cucumberResultsCommand = getOrCreateConstantCommand(graph, logger, cucumberResults); - let testExecutionIssueKeyCommand: Command | undefined = undefined; - const issueData = await getOrCall(options.jira.testExecutionIssue); - if (issueData?.key ?? options.jira.testExecutionIssueKey) { - testExecutionIssueKeyCommand = getOrCreateConstantCommand( - graph, - logger, - issueData?.key ?? options.jira.testExecutionIssueKey - ); - } else if (importCypressExecutionCommand) { - // Use an optional command in case the Cypress import fails. We could then still upload - // Cucumber results. - const fallbackExecutionIssueKeyCommand = graph.place( - new FallbackCommand( - { - fallbackOn: [ComputableState.FAILED, ComputableState.SKIPPED], - fallbackValue: undefined, - }, - logger, - importCypressExecutionCommand - ) - ); - graph.connect(importCypressExecutionCommand, fallbackExecutionIssueKeyCommand, true); - testExecutionIssueKeyCommand = fallbackExecutionIssueKeyCommand; - } - importCucumberExecutionCommand = await getImportExecutionCucumberCommand( - runResult, - cucumberResultsCommand, - projectRoot, - options, - clients, + importCucumberExecutionCommand = getImportExecutionCucumberCommand( graph, - logger, - testExecutionIssueKeyCommand + clients, + builder, + { + cucumberReportPath: options.cucumber?.preprocessor?.json.output, + projectRoot: projectRoot, + reusesExecutionIssue: + issueData?.key !== undefined || + options.jira.testExecutionIssueKey !== undefined, + testEnvironments: options.xray.testEnvironments, + testExecutionIssueKeyCommand: importCypressExecutionCommand, + testPlanIssueKey: testPlanIssueKey, + } ); // Make sure to add an edge from any feature file imports to the execution. Otherwise, the // execution will contain old steps (those which were there prior to feature import). - if (options.cucumber.uploadFeatures) { + if (options.cucumber?.uploadFeatures) { for (const importFeatureCommand of graph.getVertices()) { if (importFeatureCommand instanceof ImportFeatureCommand) { const filePath = path.relative( @@ -135,7 +117,7 @@ export async function addUploadCommands( importFeatureCommand.getParameters().filePath ); if ( - runResult.runs.some((run) => { + results.runs.some((run) => { const specPath = path.relative(projectRoot, run.spec.relative); return specPath === filePath; }) @@ -148,455 +130,684 @@ export async function addUploadCommands( } } } - addPostUploadCommands( - cypressResultsCommand, - options, - clients, - graph, - logger, - importCypressExecutionCommand, - importCucumberExecutionCommand - ); + let fallbackCypressUploadCommand; + let fallbackCucumberUploadCommand; + if (importCypressExecutionCommand) { + fallbackCypressUploadCommand = builder.addFallbackCommand({ + fallbackOn: [ComputableState.FAILED, ComputableState.SKIPPED], + fallbackValue: undefined, + input: importCypressExecutionCommand, + }); + } + if (importCucumberExecutionCommand) { + fallbackCucumberUploadCommand = builder.addFallbackCommand({ + fallbackOn: [ComputableState.FAILED, ComputableState.SKIPPED], + fallbackValue: undefined, + input: importCucumberExecutionCommand, + }); + } + const finalExecutionIssueKey = builder.addVerifyResultUploadCommand({ + cucumberExecutionIssueKey: fallbackCucumberUploadCommand, + cypressExecutionIssueKey: fallbackCypressUploadCommand, + }); + if (options.jira.attachVideos) { + builder.addAttachVideosCommand({ resolvedExecutionIssueKey: finalExecutionIssueKey }); + } + // Workaround for: https://jira.atlassian.com/browse/JRASERVER-66881. + if ( + issueData?.transition && + !(issueData.key ?? options.jira.testExecutionIssueKey) && + clients.kind === "server" + ) { + builder.addTransitionIssueCommand({ + issueKey: finalExecutionIssueKey, + transition: issueData.transition, + }); + } } -async function getImportExecutionCypressCommand( - cypressResultsCommand: Command, - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, - evidenceCollection: EvidenceCollection, +function getImportExecutionCypressCommand( graph: ExecutableGraph, - logger: Logger + clients: ClientCombination, + builder: AfterRunBuilder, + options: { + reusesExecutionIssue: boolean; + testEnvironments?: string[]; + testPlanIssueKey?: string; + } ) { - const convertCypressTestsCommand = graph.place( - new ConvertCypressTestsCommand( - { - cucumber: options.cucumber, - evidenceCollection: evidenceCollection, - jira: options.jira, - plugin: options.plugin, - useCloudStatusFallback: clients.kind === "cloud", - xray: options.xray, - }, - logger, - cypressResultsCommand - ) - ); - graph.connect(cypressResultsCommand, convertCypressTestsCommand); - const convertMultipartInfoCommand = await getConvertMultipartInfoCommand( - options, - clients, - graph, - logger, - cypressResultsCommand - ); - - const issueData = await getOrCall(options.jira.testExecutionIssue); - const combineResultsJsonCommand = graph.place( - new CombineCypressJsonCommand( - { - testExecutionIssueKey: issueData?.key ?? options.jira.testExecutionIssueKey, - }, - logger, - convertCypressTestsCommand, - convertMultipartInfoCommand - ) - ); - graph.connect(convertCypressTestsCommand, combineResultsJsonCommand); - graph.connect(convertMultipartInfoCommand, combineResultsJsonCommand); - const assertConversionValidCommand = graph.place( - new AssertCypressConversionValidCommand(logger, combineResultsJsonCommand) - ); - graph.connect(combineResultsJsonCommand, assertConversionValidCommand); - const importCypressExecutionCommand = graph.place( - new ImportExecutionCypressCommand( - { xrayClient: clients.xrayClient }, - logger, - combineResultsJsonCommand - ) - ); + const convertCypressTestsCommand = builder.addConvertCypressTestsCommand(); + const convertMultipartInfoCommand = addConvertMultipartInfoCommand(graph, clients, builder, { + testEnvironments: options.testEnvironments, + testPlanIssueKey: options.testPlanIssueKey, + }); + const combineResultsJsonCommand = builder.addCombineCypressJsonCommand({ + convertCypressTestsCommand: convertCypressTestsCommand, + convertMultipartInfoCommand: convertMultipartInfoCommand, + }); + const assertConversionValidCommand = builder.addAssertCypressConversionValidCommand({ + xrayTestExecutionResults: combineResultsJsonCommand, + }); + const importCypressExecutionCommand = builder.addImportExecutionCypressCommand({ + execution: combineResultsJsonCommand, + }); graph.connect(assertConversionValidCommand, importCypressExecutionCommand); - graph.connect(combineResultsJsonCommand, importCypressExecutionCommand); - if (issueData?.key ?? options.jira.testExecutionIssueKey) { - const verifyExecutionIssueKeyCommand = graph.place( - new VerifyExecutionIssueKeyCommand( - { - displayCloudHelp: clients.kind === "cloud", - importType: "cypress", - testExecutionIssueKey: issueData?.key ?? options.jira.testExecutionIssueKey, - testExecutionIssueType: issueData?.fields?.issuetype ?? { - name: options.jira.testExecutionIssueType, - }, - }, - logger, - importCypressExecutionCommand - ) - ); - graph.connect(importCypressExecutionCommand, verifyExecutionIssueKeyCommand); + if (options.reusesExecutionIssue) { + builder.addVerifyExecutionIssueKeyCommand({ + importType: "cypress", + resolvedExecutionIssue: importCypressExecutionCommand, + }); } return importCypressExecutionCommand; } -async function getImportExecutionCucumberCommand( - runResult: CypressRunResultType, - cucumberResultsCommand: ConstantCommand, - projectRoot: string, - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, +function getImportExecutionCucumberCommand( graph: ExecutableGraph, - logger: Logger, - testExecutionIssueKeyCommand?: Command + clients: ClientCombination, + builder: AfterRunBuilder, + options: { + cucumberReportPath?: string; + projectRoot: string; + reusesExecutionIssue: boolean; + testEnvironments?: string[]; + testExecutionIssueKeyCommand?: Command; + testPlanIssueKey?: string; + } ) { - const cypressResultsCommand = getOrCreateConstantCommand(graph, logger, runResult); - const convertMultipartInfoCommand = await getConvertMultipartInfoCommand( - options, - clients, - graph, - logger, - cypressResultsCommand - ); - const convertCucumberFeaturesCommand = graph.place( - new ConvertCucumberFeaturesCommand( - { - cucumber: { - prefixes: { - precondition: options.cucumber?.prefixes.precondition, - test: options.cucumber?.prefixes.test, - }, - }, - jira: { - projectKey: options.jira.projectKey, - }, - projectRoot: projectRoot, - useCloudTags: clients.kind === "cloud", - xray: { - status: options.xray.status, - testEnvironments: options.xray.testEnvironments, - uploadScreenshots: options.xray.uploadScreenshots, - }, - }, - logger, - cucumberResultsCommand, - testExecutionIssueKeyCommand - ) - ); - graph.connect(cucumberResultsCommand, convertCucumberFeaturesCommand); - if (testExecutionIssueKeyCommand) { - graph.connect(testExecutionIssueKeyCommand, convertCucumberFeaturesCommand); - } - const combineCucumberMultipartCommand = graph.place( - new CombineCucumberMultipartCommand( - logger, - convertMultipartInfoCommand, - convertCucumberFeaturesCommand - ) - ); - graph.connect(convertMultipartInfoCommand, combineCucumberMultipartCommand); - graph.connect(convertCucumberFeaturesCommand, combineCucumberMultipartCommand); - const assertConversionValidCommand = graph.place( - new AssertCucumberConversionValidCommand(logger, combineCucumberMultipartCommand) - ); - graph.connect(combineCucumberMultipartCommand, assertConversionValidCommand); - const importCucumberExecutionCommand = graph.place( - new ImportExecutionCucumberCommand( - { xrayClient: clients.xrayClient }, - logger, - combineCucumberMultipartCommand - ) - ); - graph.connect(assertConversionValidCommand, importCucumberExecutionCommand); - graph.connect(combineCucumberMultipartCommand, importCucumberExecutionCommand); - const issueData = await getOrCall(options.jira.testExecutionIssue); - if (issueData?.key ?? options.jira.testExecutionIssueKey) { - const verifyExecutionIssueKeyCommand = graph.place( - new VerifyExecutionIssueKeyCommand( - { - displayCloudHelp: clients.kind === "cloud", - importType: "cucumber", - testExecutionIssueKey: issueData?.key ?? options.jira.testExecutionIssueKey, - testExecutionIssueType: issueData?.fields?.issuetype ?? { - name: options.jira.testExecutionIssueType, - }, - }, - logger, - importCucumberExecutionCommand - ) + if (!options.cucumberReportPath) { + throw new Error( + "Failed to prepare Cucumber upload: Cucumber preprocessor JSON report path not configured." ); - graph.connect(importCucumberExecutionCommand, verifyExecutionIssueKeyCommand); + } + const convertMultipartInfoCommand = addConvertMultipartInfoCommand(graph, clients, builder, { + testEnvironments: options.testEnvironments, + testPlanIssueKey: options.testPlanIssueKey, + }); + const cucumberResultsCommand = builder.getCucumberResultsCommand({ + cucumberReportPath: options.cucumberReportPath, + projectRoot: options.projectRoot, + }); + const convertCucumberFeaturesCommand = builder.addConvertCucumberFeaturesCommand({ + cucumberResults: cucumberResultsCommand, + projectRoot: options.projectRoot, + testExecutionIssueKeyCommand: options.testExecutionIssueKeyCommand, + }); + const combineCucumberMultipartCommand = builder.addCombineCucumberMultipartCommand({ + cucumberMultipartFeatures: convertCucumberFeaturesCommand, + cucumberMultipartInfo: convertMultipartInfoCommand, + }); + const assertConversionValidCommand = builder.addAssertCucumberConversionValidCommand({ + cucumberMultipart: combineCucumberMultipartCommand, + }); + const importCucumberExecutionCommand = builder.addImportExecutionCucumberCommand({ + cucumberMultipart: combineCucumberMultipartCommand, + }); + graph.connect(assertConversionValidCommand, importCucumberExecutionCommand); + if (options.reusesExecutionIssue) { + builder.addVerifyExecutionIssueKeyCommand({ + importType: "cucumber", + resolvedExecutionIssue: importCucumberExecutionCommand, + }); } return importCucumberExecutionCommand; } -async function getExtractExecutionIssueTypeCommand( - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, +function addConvertMultipartInfoCommand( graph: ExecutableGraph, - logger: Logger + clients: ClientCombination, + builder: AfterRunBuilder, + options: { + testEnvironments?: string[]; + testPlanIssueKey?: string; + } ) { - const issueData = await getOrCall(options.jira.testExecutionIssue); - if (issueData?.fields?.issuetype) { - return getOrCreateConstantCommand(graph, logger, issueData.fields.issuetype); + let convertCommand; + if (clients.kind === "cloud") { + convertCommand = graph.find((command) => command instanceof ConvertInfoCloudCommand); + } else { + convertCommand = graph.find((command) => command instanceof ConvertInfoServerCommand); } - const fetchIssueTypesCommand = graph.findOrDefault(FetchIssueTypesCommand, () => - graph.place(new FetchIssueTypesCommand({ jiraClient: clients.jiraClient }, logger)) - ); - return graph.findOrDefault(ExtractExecutionIssueTypeCommand, () => { - const extractExecutionIssueTypeCommand = graph.place( - new ExtractExecutionIssueTypeCommand( + if (convertCommand) { + return convertCommand; + } + if (clients.kind === "cloud") { + convertCommand = builder.addConvertInfoCloudCommand({ + testPlanIssueKey: options.testPlanIssueKey, + }); + } else { + let testPlanIdCommand: Command | undefined = undefined; + let testEnvironmentsIdCommand: Command | undefined = undefined; + if (options.testPlanIssueKey) { + testPlanIdCommand = builder.addExtractFieldIdCommand("test-plan"); + } + if (options.testEnvironments) { + testEnvironmentsIdCommand = builder.addExtractFieldIdCommand("test-environments"); + } + convertCommand = builder.addConvertInfoServerCommand({ + fieldIds: { + testEnvironment: testEnvironmentsIdCommand, + testPlan: testPlanIdCommand, + }, + testPlanIssueKey: options.testPlanIssueKey, + }); + } + return convertCommand; +} + +class AfterRunBuilder { + private readonly graph: ExecutableGraph; + private readonly results: CypressRunResultType; + private readonly options: InternalCypressXrayPluginOptions; + private readonly issueData: PluginIssueUpdate | undefined; + private readonly evidenceCollection: EvidenceCollection; + private readonly clients: ClientCombination; + private readonly logger: Logger; + private readonly constants: { + executionIssue?: { + issuetype?: Command; + issueUpdate?: Command; + summary?: Command; + }; + results?: ConstantCommand; + }; + + constructor(args: { + clients: ClientCombination; + evidenceCollection: EvidenceCollection; + graph: ExecutableGraph; + issueData?: PluginIssueUpdate; + logger: Logger; + options: InternalCypressXrayPluginOptions; + results: CypressRunResultType; + }) { + this.graph = args.graph; + this.results = args.results; + this.options = args.options; + this.issueData = args.issueData; + this.evidenceCollection = args.evidenceCollection; + this.clients = args.clients; + this.logger = args.logger; + this.constants = {}; + } + + public getCucumberResultsCommand(parameters: { + cucumberReportPath: string; + projectRoot: string; + }) { + // Cypress might change process.cwd(), so we need to query the root directory. + // See: https://github.com/cypress-io/cypress/issues/22689 + const reportPath = path.resolve(parameters.projectRoot, parameters.cucumberReportPath); + const cucumberResults = JSON.parse( + fs.readFileSync(reportPath, "utf-8") + ) as CucumberMultipartFeature[]; + return getOrCreateConstantCommand(this.graph, this.logger, cucumberResults); + } + + public addConvertCypressTestsCommand() { + const resultsCommand = this.getResultsCommand(); + const command = this.graph.place( + new ConvertCypressTestsCommand( { - displayCloudHelp: clients.kind === "cloud", - projectKey: options.jira.projectKey, - testExecutionIssueType: issueData?.fields?.issuetype ?? { - name: options.jira.testExecutionIssueType, - }, + evidenceCollection: this.evidenceCollection, + featureFileExtension: this.options.cucumber?.featureFileExtension, + normalizeScreenshotNames: this.options.plugin.normalizeScreenshotNames, + projectKey: this.options.jira.projectKey, + uploadScreenshots: this.options.xray.uploadScreenshots, + useCloudStatusFallback: this.clients.kind === "cloud", + xrayStatus: this.options.xray.status, }, - logger, - fetchIssueTypesCommand + this.logger, + resultsCommand ) ); - graph.connect(fetchIssueTypesCommand, extractExecutionIssueTypeCommand); - return extractExecutionIssueTypeCommand; - }); -} + this.graph.connect(resultsCommand, command); + return command; + } -async function getExecutionIssueSummaryCommand( - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, - graph: ExecutableGraph, - logger: Logger -) { - const issueData = await getOrCall(options.jira.testExecutionIssue); - const testExecutionIssueSummary = - issueData?.fields?.summary ?? options.jira.testExecutionIssueSummary; - if (testExecutionIssueSummary) { - return getOrCreateConstantCommand(graph, logger, testExecutionIssueSummary); - } - const testExecutionIssueKey = issueData?.key ?? options.jira.testExecutionIssueKey; - if (testExecutionIssueKey) { - const issueKeysCommand = getOrCreateConstantCommand(graph, logger, [testExecutionIssueKey]); - const getSummaryValuesCommand = graph.findOrDefault( - GetSummaryValuesCommand, - () => { - const command = graph.place( - new GetSummaryValuesCommand( - { jiraClient: clients.jiraClient }, - logger, - issueKeysCommand - ) - ); - graph.connect(issueKeysCommand, command); - return command; - }, - (vertex) => [...graph.getPredecessors(vertex)].includes(issueKeysCommand) - ); - const destructureCommand = graph.place( - new DestructureCommand(logger, getSummaryValuesCommand, testExecutionIssueKey) + public addCombineCypressJsonCommand(parameters: { + convertCypressTestsCommand: Command<[XrayTest, ...XrayTest[]]>; + convertMultipartInfoCommand: Command; + }) { + const command = this.graph.place( + new CombineCypressJsonCommand( + { + testExecutionIssueKey: + this.issueData?.key ?? this.options.jira.testExecutionIssueKey, + }, + this.logger, + parameters.convertCypressTestsCommand, + parameters.convertMultipartInfoCommand + ) ); - graph.connect(getSummaryValuesCommand, destructureCommand); - return destructureCommand; + this.graph.connect(parameters.convertCypressTestsCommand, command); + this.graph.connect(parameters.convertMultipartInfoCommand, command); + return command; } -} -async function getConvertMultipartInfoCommand( - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, - graph: ExecutableGraph, - logger: Logger, - cypressResultsCommand: Command -) { - let convertCommand: ConvertInfoCommand | undefined; - if (clients.kind === "cloud") { - convertCommand = graph.find( - (command): command is ConvertInfoCloudCommand => - command instanceof ConvertInfoCloudCommand && - [...graph.getPredecessors(command)].includes(cypressResultsCommand) + public addAssertCypressConversionValidCommand(parameters: { + xrayTestExecutionResults: Command>; + }) { + const command = this.graph.place( + new AssertCypressConversionValidCommand( + this.logger, + parameters.xrayTestExecutionResults + ) ); - } else { - convertCommand = graph.find( - (command): command is ConvertInfoServerCommand => - command instanceof ConvertInfoServerCommand && - [...graph.getPredecessors(command)].includes(cypressResultsCommand) + this.graph.connect(parameters.xrayTestExecutionResults, command); + return command; + } + + public addImportExecutionCypressCommand(parameters: { + execution: Command>; + }) { + const command = this.graph.place( + new ImportExecutionCypressCommand( + { xrayClient: this.clients.xrayClient }, + this.logger, + parameters.execution + ) ); + this.graph.connect(parameters.execution, command); + return command; } - if (convertCommand) { - return convertCommand; + + public addExtractFieldIdCommand(field: "test-environments" | "test-plan") { + switch (field) { + case "test-plan": + return this.options.jira.fields.testPlan + ? getOrCreateConstantCommand( + this.graph, + this.logger, + this.options.jira.fields.testPlan + ) + : getOrCreateExtractFieldIdCommand( + JiraField.TEST_PLAN, + this.clients.jiraClient, + this.graph, + this.logger + ); + case "test-environments": + return this.options.jira.fields.testEnvironments + ? getOrCreateConstantCommand( + this.graph, + this.logger, + this.options.jira.fields.testEnvironments + ) + : getOrCreateExtractFieldIdCommand( + JiraField.TEST_ENVIRONMENTS, + this.clients.jiraClient, + this.graph, + this.logger + ); + } } - let textExecutionIssueDataCommand: Command> | undefined; - if (options.jira.testExecutionIssue) { - textExecutionIssueDataCommand = getOrCreateConstantCommand( - graph, - logger, - options.jira.testExecutionIssue + + public addConvertInfoCloudCommand(parameters: { testPlanIssueKey?: string }) { + const resultsCommand = this.getResultsCommand(); + const issueData = this.getIssueData(); + const command = new ConvertInfoCloudCommand( + { + jira: { + projectKey: this.options.jira.projectKey, + testPlanIssueKey: parameters.testPlanIssueKey, + }, + xray: this.options.xray, + }, + this.logger, + { + issuetype: issueData.issuetype, + issueUpdate: issueData.issueUpdate, + results: resultsCommand, + summary: issueData.summary, + } ); + this.graph.place(command); + this.graph.connect(resultsCommand, command); + this.graph.connect(issueData.summary, command); + this.graph.connect(issueData.issuetype, command); + if (issueData.issueUpdate) { + this.graph.connect(issueData.issueUpdate, command); + } + return command; } - const executionIssueSummaryCommand = await getExecutionIssueSummaryCommand( - options, - clients, - graph, - logger - ); - const extractExecutionIssueTypeCommand = await getExtractExecutionIssueTypeCommand( - options, - clients, - graph, - logger - ); - if (clients.kind === "cloud") { - convertCommand = graph.place( - new ConvertInfoCloudCommand( - { jira: options.jira, xray: options.xray }, - logger, - extractExecutionIssueTypeCommand, - cypressResultsCommand, - { - custom: textExecutionIssueDataCommand, - summary: executionIssueSummaryCommand, - } - ) + + public addConvertInfoServerCommand(parameters: { + fieldIds: { + testEnvironment?: Command; + testPlan?: Command; + }; + testPlanIssueKey?: string; + }) { + const resultsCommand = this.getResultsCommand(); + const issueData = this.getIssueData(); + const command = new ConvertInfoServerCommand( + { + jira: { + projectKey: this.options.jira.projectKey, + testPlanIssueKey: parameters.testPlanIssueKey, + }, + xray: this.options.xray, + }, + this.logger, + { + fieldIds: { + testEnvironmentsId: parameters.fieldIds.testEnvironment, + testPlanId: parameters.fieldIds.testPlan, + }, + issuetype: issueData.issuetype, + issueUpdate: issueData.issueUpdate, + results: resultsCommand, + summary: issueData.summary, + } ); - } else { - let testPlanIdCommand: Command | undefined = undefined; - let testEnvironmentsIdCommand: Command | undefined = undefined; - if (options.jira.testPlanIssueKey) { - testPlanIdCommand = options.jira.fields.testPlan - ? getOrCreateConstantCommand(graph, logger, options.jira.fields.testPlan) - : getOrCreateExtractFieldIdCommand( - JiraField.TEST_PLAN, - clients.jiraClient, - graph, - logger - ); + this.graph.place(command); + this.graph.connect(resultsCommand, command); + this.graph.connect(issueData.summary, command); + this.graph.connect(issueData.issuetype, command); + if (issueData.issueUpdate) { + this.graph.connect(issueData.issueUpdate, command); + } + if (parameters.fieldIds.testEnvironment) { + this.graph.connect(parameters.fieldIds.testEnvironment, command); } - if (options.xray.testEnvironments) { - testEnvironmentsIdCommand = options.jira.fields.testEnvironments - ? getOrCreateConstantCommand(graph, logger, options.jira.fields.testEnvironments) - : getOrCreateExtractFieldIdCommand( - JiraField.TEST_ENVIRONMENTS, - clients.jiraClient, - graph, - logger - ); + if (parameters.fieldIds.testPlan) { + this.graph.connect(parameters.fieldIds.testPlan, command); + } + return command; + } + + public addConvertCucumberFeaturesCommand(parameters: { + cucumberResults: Command; + projectRoot: string; + testExecutionIssueKeyCommand?: Command; + }) { + let resolvedExecutionIssueKeyCommand; + if (parameters.testExecutionIssueKeyCommand) { + resolvedExecutionIssueKeyCommand = parameters.testExecutionIssueKeyCommand; + } else { + const executionIssueKey = + this.issueData?.key ?? this.options.jira.testExecutionIssueKey; + if (executionIssueKey) { + resolvedExecutionIssueKeyCommand = getOrCreateConstantCommand( + this.graph, + this.logger, + executionIssueKey + ); + } } - convertCommand = graph.place( - new ConvertInfoServerCommand( - { jira: options.jira, xray: options.xray }, - logger, - extractExecutionIssueTypeCommand, - cypressResultsCommand, + const command = this.graph.place( + new ConvertCucumberFeaturesCommand( { - custom: textExecutionIssueDataCommand, - fieldIds: { - testEnvironmentsId: testEnvironmentsIdCommand, - testPlanId: testPlanIdCommand, + cucumber: { + prefixes: { + precondition: this.options.cucumber?.prefixes.precondition, + test: this.options.cucumber?.prefixes.test, + }, + }, + jira: { + projectKey: this.options.jira.projectKey, + }, + projectRoot: parameters.projectRoot, + useCloudTags: this.clients.kind === "cloud", + xray: { + status: this.options.xray.status, + testEnvironments: this.options.xray.testEnvironments, + uploadScreenshots: this.options.xray.uploadScreenshots, }, - summary: executionIssueSummaryCommand, + }, + this.logger, + { + cucumberResults: parameters.cucumberResults, + testExecutionIssueKey: resolvedExecutionIssueKeyCommand, } ) ); - if (testPlanIdCommand) { - graph.connect(testPlanIdCommand, convertCommand); - } - if (testEnvironmentsIdCommand) { - graph.connect(testEnvironmentsIdCommand, convertCommand); + this.graph.connect(parameters.cucumberResults, command); + if (resolvedExecutionIssueKeyCommand) { + this.graph.connect(resolvedExecutionIssueKeyCommand, command); } + return command; } - if (textExecutionIssueDataCommand) { - graph.connect(textExecutionIssueDataCommand, convertCommand); + + public addCombineCucumberMultipartCommand(parameters: { + cucumberMultipartFeatures: Command; + cucumberMultipartInfo: Command; + }) { + const command = this.graph.place( + new CombineCucumberMultipartCommand( + this.logger, + parameters.cucumberMultipartInfo, + parameters.cucumberMultipartFeatures + ) + ); + this.graph.connect(parameters.cucumberMultipartInfo, command); + this.graph.connect(parameters.cucumberMultipartFeatures, command); + return command; } - graph.connect(extractExecutionIssueTypeCommand, convertCommand); - graph.connect(cypressResultsCommand, convertCommand); - if (executionIssueSummaryCommand) { - graph.connect(executionIssueSummaryCommand, convertCommand); + + public addAssertCucumberConversionValidCommand(parameters: { + cucumberMultipart: Command; + }) { + const command = this.graph.place( + new AssertCucumberConversionValidCommand(this.logger, parameters.cucumberMultipart) + ); + this.graph.connect(parameters.cucumberMultipart, command); + return command; } - return convertCommand; -} -function addPostUploadCommands( - cypressResultsCommand: Command, - options: InternalCypressXrayPluginOptions, - clients: ClientCombination, - graph: ExecutableGraph, - logger: Logger, - importCypressExecutionCommand: ImportExecutionCypressCommand | null, - importCucumberExecutionCommand: ImportExecutionCucumberCommand | null -): void { - let fallbackCypressUploadCommand: Command | undefined = undefined; - let fallbackCucumberUploadCommand: Command | undefined = undefined; - if (importCypressExecutionCommand) { - fallbackCypressUploadCommand = graph.findOrDefault( - FallbackCommand, - () => { - const fallbackCommand = graph.place( - new FallbackCommand( - { - fallbackOn: [ComputableState.FAILED, ComputableState.SKIPPED], - fallbackValue: undefined, - }, - logger, - importCypressExecutionCommand - ) - ); - graph.connect(importCypressExecutionCommand, fallbackCommand, true); - return fallbackCommand; - }, - (command) => { - const predecessors = [...graph.getPredecessors(command)]; - return ( - predecessors.length === 1 && predecessors[0] === importCypressExecutionCommand - ); - } + public addVerifyExecutionIssueKeyCommand(parameters: { + importType: "cucumber" | "cypress"; + resolvedExecutionIssue: Command; + }) { + const command = this.graph.place( + new VerifyExecutionIssueKeyCommand( + { + displayCloudHelp: this.clients.kind === "cloud", + importType: parameters.importType, + testExecutionIssueKey: + this.issueData?.key ?? this.options.jira.testExecutionIssueKey, + testExecutionIssueType: this.issueData?.fields?.issuetype ?? { + name: this.options.jira.testExecutionIssueType, + }, + }, + this.logger, + parameters.resolvedExecutionIssue + ) ); + this.graph.connect(parameters.resolvedExecutionIssue, command); + return command; } - if (importCucumberExecutionCommand) { - fallbackCucumberUploadCommand = graph.findOrDefault( - FallbackCommand, + + public addImportExecutionCucumberCommand(parameters: { + cucumberMultipart: Command; + }) { + const command = this.graph.place( + new ImportExecutionCucumberCommand( + { xrayClient: this.clients.xrayClient }, + this.logger, + parameters.cucumberMultipart + ) + ); + this.graph.connect(parameters.cucumberMultipart, command); + return command; + } + + public addFallbackCommand(parameters: { + fallbackOn: ComputableState[]; + fallbackValue: V; + input: Command; + }) { + return this.graph.findOrDefault( + FallbackCommand, () => { - const fallbackCommand = graph.place( - new FallbackCommand( + const command = this.graph.place( + new FallbackCommand( { - fallbackOn: [ComputableState.FAILED, ComputableState.SKIPPED], - fallbackValue: undefined, + fallbackOn: parameters.fallbackOn, + fallbackValue: parameters.fallbackValue, }, - logger, - importCucumberExecutionCommand + this.logger, + parameters.input ) ); - graph.connect(importCucumberExecutionCommand, fallbackCommand, true); - return fallbackCommand; + this.graph.connect(parameters.input, command, true); + return command; }, (command) => { - const predecessors = [...graph.getPredecessors(command)]; - return ( - predecessors.length === 1 && predecessors[0] === importCucumberExecutionCommand - ); + const predecessors = [...this.graph.getPredecessors(command)]; + return predecessors.length === 1 && predecessors[0] === parameters.input; } ); } - const verifyResultsUploadCommand = graph.place( - new VerifyResultsUploadCommand({ url: options.jira.url }, logger, { - cucumberExecutionIssueKey: fallbackCucumberUploadCommand, - cypressExecutionIssueKey: fallbackCypressUploadCommand, - }) - ); - if (fallbackCypressUploadCommand) { - graph.connect(fallbackCypressUploadCommand, verifyResultsUploadCommand); - } - if (fallbackCucumberUploadCommand) { - graph.connect(fallbackCucumberUploadCommand, verifyResultsUploadCommand); + + public addVerifyResultUploadCommand(parameters: { + cucumberExecutionIssueKey?: Command; + cypressExecutionIssueKey?: Command; + }) { + const command = this.graph.place( + new VerifyResultsUploadCommand({ url: this.options.jira.url }, this.logger, { + cucumberExecutionIssueKey: parameters.cucumberExecutionIssueKey, + cypressExecutionIssueKey: parameters.cypressExecutionIssueKey, + }) + ); + if (parameters.cypressExecutionIssueKey) { + this.graph.connect(parameters.cypressExecutionIssueKey, command); + } + if (parameters.cucumberExecutionIssueKey) { + this.graph.connect(parameters.cucumberExecutionIssueKey, command); + } + return command; } - if (options.jira.attachVideos) { - const extractVideoFilesCommand = graph.place( - new ExtractVideoFilesCommand(logger, cypressResultsCommand) + + public addAttachVideosCommand(parameters: { resolvedExecutionIssueKey: Command }) { + const resultsCommand = this.getResultsCommand(); + const extractVideoFilesCommand = this.graph.place( + new ExtractVideoFilesCommand(this.logger, resultsCommand) ); - graph.connect(cypressResultsCommand, extractVideoFilesCommand); - const attachVideosCommand = graph.place( + this.graph.connect(resultsCommand, extractVideoFilesCommand); + const command = this.graph.place( new AttachFilesCommand( - { jiraClient: clients.jiraClient }, - logger, + { jiraClient: this.clients.jiraClient }, + this.logger, extractVideoFilesCommand, - verifyResultsUploadCommand + parameters.resolvedExecutionIssueKey ) ); - graph.connect(extractVideoFilesCommand, attachVideosCommand); - graph.connect(verifyResultsUploadCommand, attachVideosCommand); + this.graph.connect(extractVideoFilesCommand, command); + this.graph.connect(parameters.resolvedExecutionIssueKey, command); + return command; + } + + public addTransitionIssueCommand(parameters: { + issueKey: Command; + transition: IssueTransition; + }) { + const command = this.graph.place( + new TransitionIssueCommand( + { + jiraClient: this.clients.jiraClient, + transition: parameters.transition, + }, + this.logger, + parameters.issueKey + ) + ); + this.graph.connect(parameters.issueKey, command); + return command; + } + + private getIssueData() { + let issueUpdateCommand; + let summaryCommand = this.constants.executionIssue?.summary; + let issuetypeCommand = this.constants.executionIssue?.issuetype; + if (!this.constants.executionIssue?.issueUpdate && this.issueData) { + issueUpdateCommand = getOrCreateConstantCommand( + this.graph, + this.logger, + this.issueData + ); + this.constants.executionIssue = { + ...this.constants.executionIssue, + issueUpdate: issueUpdateCommand, + }; + } + if (!summaryCommand) { + const summary = + this.issueData?.fields?.summary ?? this.options.jira.testExecutionIssueSummary; + if (summary) { + summaryCommand = getOrCreateConstantCommand(this.graph, this.logger, summary); + } else { + const testExecutionIssueKey = + this.issueData?.key ?? this.options.jira.testExecutionIssueKey; + if (testExecutionIssueKey) { + const issueKeysCommand = getOrCreateConstantCommand(this.graph, this.logger, [ + testExecutionIssueKey, + ]); + const getSummaryValuesCommand = this.graph.findOrDefault( + GetSummaryValuesCommand, + () => { + const command = this.graph.place( + new GetSummaryValuesCommand( + { jiraClient: this.clients.jiraClient }, + this.logger, + issueKeysCommand + ) + ); + this.graph.connect(issueKeysCommand, command); + return command; + }, + (vertex) => + [...this.graph.getPredecessors(vertex)].includes(issueKeysCommand) + ); + summaryCommand = this.graph.place( + new DestructureCommand( + this.logger, + getSummaryValuesCommand, + testExecutionIssueKey + ) + ); + this.graph.connect(getSummaryValuesCommand, summaryCommand); + } else { + summaryCommand = getOrCreateConstantCommand( + this.graph, + this.logger, + `Execution Results [${this.results.startedTestsAt}]` + ); + } + } + this.constants.executionIssue = { + ...this.constants.executionIssue, + summary: summaryCommand, + }; + } + if (!issuetypeCommand) { + issuetypeCommand = getOrCreateConstantCommand( + this.graph, + this.logger, + this.issueData?.fields?.issuetype ?? { + name: this.options.jira.testExecutionIssueType, + } + ); + this.constants.executionIssue = { + ...this.constants.executionIssue, + issuetype: issuetypeCommand, + }; + } + return { + issuetype: issuetypeCommand, + issueUpdate: issueUpdateCommand, + summary: summaryCommand, + }; + } + + private getResultsCommand() { + if (!this.constants.results) { + this.constants.results = getOrCreateConstantCommand( + this.graph, + this.logger, + this.results + ); + } + return this.constants.results; } } diff --git a/src/hooks/after/commands/conversion/convert-info-command.spec.ts b/src/hooks/after/commands/conversion/convert-info-command.spec.ts index 69757a21..20e3829e 100644 --- a/src/hooks/after/commands/conversion/convert-info-command.spec.ts +++ b/src/hooks/after/commands/conversion/convert-info-command.spec.ts @@ -16,15 +16,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, { id: "issue_1578" }), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:36.177Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, { id: "issue_1578" }), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:36.177Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "Execution Results [1694257168829]"), } ); @@ -56,18 +56,18 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { fieldIds: { testPlanId: new ConstantCommand(logger, "customfield_12345"), }, + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ); @@ -85,18 +85,18 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { testEnvironments: ["DEV", "PROD"], uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { fieldIds: { testEnvironmentsId: new ConstantCommand(logger, "customfield_45678"), }, + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ); @@ -117,15 +117,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ) @@ -144,15 +144,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { testEnvironments: ["DEV", "PROD"], uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ) @@ -171,15 +171,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ); @@ -203,15 +203,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, { id: "issue_1578" }), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, { id: "issue_1578" }), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "Execution Results [1694257168829]"), } ); @@ -247,15 +247,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ); @@ -276,15 +276,15 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { testEnvironments: ["DEV", "PROD"], uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, {}), - new ConstantCommand(logger, { - browserName: "Firefox", - browserVersion: "123.11.6", - cypressVersion: "42.4.9", - endedTestsAt: "2023-09-09T10:59:31.416Z", - startedTestsAt: "2023-09-09T10:59:28.829Z", - }), { + issuetype: new ConstantCommand(logger, {}), + results: new ConstantCommand(logger, { + browserName: "Firefox", + browserVersion: "123.11.6", + cypressVersion: "42.4.9", + endedTestsAt: "2023-09-09T10:59:31.416Z", + startedTestsAt: "2023-09-09T10:59:28.829Z", + }), summary: new ConstantCommand(logger, "my summary"), } ); diff --git a/src/hooks/after/commands/conversion/convert-info-command.ts b/src/hooks/after/commands/conversion/convert-info-command.ts index 7b92a258..88bca3f6 100644 --- a/src/hooks/after/commands/conversion/convert-info-command.ts +++ b/src/hooks/after/commands/conversion/convert-info-command.ts @@ -1,7 +1,6 @@ import { IssueTypeDetails } from "../../../../types/jira/responses/issue-type-details"; import { IssueUpdate } from "../../../../types/jira/responses/issue-update"; -import { InternalJiraOptions, InternalXrayOptions } from "../../../../types/plugin"; -import { MaybeFunction } from "../../../../types/util"; +import { InternalXrayOptions } from "../../../../types/plugin"; import { MultipartInfo } from "../../../../types/xray/requests/import-execution-multipart-info"; import { getOrCall } from "../../../../util/functions"; import { Logger } from "../../../../util/logging"; @@ -15,55 +14,59 @@ import { } from "./util/multipart-info"; interface Parameters { - jira: Pick & { - testExecutionIssueDescription?: string; + jira: { + projectKey: string; + testPlanIssueKey?: string; }; xray: Pick; } -export abstract class ConvertInfoCommand extends Command { - private readonly testExecutionIssueType: Computable; - private readonly runInformation: Computable; - private readonly info?: { - custom?: Computable>; - fieldIds?: { - testEnvironmentsId?: Computable; - testPlanId?: Computable; - }; - summary?: Computable; +export type ComputedIssueUpdate = IssueUpdate & { + computedFields: { + issuetype: Computable; + summary: Computable; }; +}; + +export abstract class ConvertInfoCommand extends Command { + private readonly results: Computable; + private readonly summary: Computable; + private readonly issuetype: Computable; + private readonly issueUpdate?: Computable; constructor( parameters: Parameters, logger: Logger, - testExecutionIssueType: Computable, - runInformation: Computable, - info?: { - custom?: Computable>; - fieldIds?: { - testEnvironmentsId?: Computable; - testPlanId?: Computable; - }; - summary?: Computable; + input: { + issuetype: Computable; + issueUpdate?: Computable; + results: Computable; + summary: Computable; } ) { super(parameters, logger); - this.info = info; - this.testExecutionIssueType = testExecutionIssueType; - this.runInformation = runInformation; + this.results = input.results; + this.issueUpdate = input.issueUpdate; + this.summary = input.summary; + this.issuetype = input.issuetype; } protected async computeResult(): Promise { - const testExecutionIssueType = await this.testExecutionIssueType.compute(); - const runInformation = await this.runInformation.compute(); - const custom = await this.info?.custom?.compute(); - const summary = await this.info?.summary?.compute(); + const runInformation = await this.results.compute(); + const issueUpdate = await this.issueUpdate?.compute(); const testExecutionIssueData: TestExecutionIssueDataServer = { - custom: await getOrCall(custom), - description: this.parameters.jira.testExecutionIssueDescription, - issuetype: testExecutionIssueType, projectKey: this.parameters.jira.projectKey, - summary: summary, + testExecutionIssue: { + fields: { + ...issueUpdate?.fields, + issuetype: await this.issuetype.compute(), + summary: await this.summary.compute(), + }, + historyMetadata: issueUpdate?.historyMetadata, + properties: issueUpdate?.properties, + transition: issueUpdate?.transition, + update: issueUpdate?.update, + }, }; return await this.buildInfo(runInformation, testExecutionIssueData); } @@ -78,23 +81,32 @@ export class ConvertInfoServerCommand extends ConvertInfoCommand { private readonly testEnvironmentsId?: Computable; private readonly testPlanId?: Computable; constructor( - ...[options, logger, testExecutionIssueType, runInformation, info]: ConstructorParameters< - typeof ConvertInfoCommand - > + parameters: Parameters, + logger: Logger, + input: { + fieldIds?: { + testEnvironmentsId?: Computable; + testPlanId?: Computable; + }; + issuetype: Computable; + issueUpdate?: Computable; + results: Computable; + summary: Computable; + } ) { - super(options, logger, testExecutionIssueType, runInformation, info); - if (this.parameters.jira.testPlanIssueKey && !info?.fieldIds?.testPlanId) { + super(parameters, logger, input); + if (this.parameters.jira.testPlanIssueKey && !input.fieldIds?.testPlanId) { throw new Error( "A test plan issue key was supplied without the test plan Jira field ID" ); } - if (this.parameters.xray.testEnvironments && !info?.fieldIds?.testEnvironmentsId) { + if (this.parameters.xray.testEnvironments && !input.fieldIds?.testEnvironmentsId) { throw new Error( "Test environments were supplied without the test environments Jira field ID" ); } - this.testEnvironmentsId = info?.fieldIds?.testEnvironmentsId; - this.testPlanId = info?.fieldIds?.testPlanId; + this.testEnvironmentsId = input.fieldIds?.testEnvironmentsId; + this.testPlanId = input.fieldIds?.testPlanId; } protected async buildInfo( diff --git a/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.spec.ts b/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.spec.ts index 12388f8c..ef05072e 100644 --- a/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.spec.ts +++ b/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.spec.ts @@ -29,7 +29,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport.slice(0, 1)) + { cucumberResults: new ConstantCommand(logger, cucumberReport.slice(0, 1)) } ); const features = await command.compute(); expect(features).to.be.an("array").with.length(1); @@ -53,7 +53,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport.slice(0, 1)) + { cucumberResults: new ConstantCommand(logger, cucumberReport.slice(0, 1)) } ); expect(command.getParameters()).to.deep.eq({ cucumber: { @@ -89,7 +89,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport.slice(0, 1)) + { cucumberResults: new ConstantCommand(logger, cucumberReport.slice(0, 1)) } ); const features = await command.compute(); expect(features).to.be.an("array").with.length(1); @@ -114,7 +114,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(features).to.be.an("array").with.length(2); @@ -141,8 +141,10 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport), - new ConstantCommand(logger, "CYP-456") + { + cucumberResults: new ConstantCommand(logger, cucumberReport), + testExecutionIssueKey: new ConstantCommand(logger, "CYP-456"), + } ); const features = await command.compute(); expect(features[0].tags).to.deep.eq([{ name: "@CYP-456" }]); @@ -169,8 +171,10 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport), - new ConstantCommand(logger, "CYP-456") + { + cucumberResults: new ConstantCommand(logger, cucumberReport), + testExecutionIssueKey: new ConstantCommand(logger, "CYP-456"), + } ); const features = await command.compute(); expect(features[0].tags).to.deep.eq([{ name: "@CYP-456" }]); @@ -195,7 +199,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: true }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expectToExist(features[0].elements[2].steps[1].embeddings); @@ -227,100 +231,102 @@ describe(path.relative(process.cwd(), __filename), () => { }, }, logger, - new ConstantCommand(logger, [ - { - description: "", - elements: [ - { - description: "", - id: "a-tagged-feature;tc---development", - keyword: "Scenario", - line: 9, - name: "TC - Development", - steps: [ - { - arguments: [], - embeddings: [], - keyword: "Given ", - line: 5, - name: "abc123", - result: { - duration: 0, - status: "undefined", + { + cucumberResults: new ConstantCommand(logger, [ + { + description: "", + elements: [ + { + description: "", + id: "a-tagged-feature;tc---development", + keyword: "Scenario", + line: 9, + name: "TC - Development", + steps: [ + { + arguments: [], + embeddings: [], + keyword: "Given ", + line: 5, + name: "abc123", + result: { + duration: 0, + status: "undefined", + }, }, - }, - { - arguments: [], - keyword: "Then ", - line: 6, - name: "xyz9871", - result: { - duration: 0, - status: "skipped", + { + arguments: [], + keyword: "Then ", + line: 6, + name: "xyz9871", + result: { + duration: 0, + status: "skipped", + }, }, - }, - { - arguments: [], - keyword: "Given ", - line: 10, - name: "an assumption", - result: { - duration: 0, - status: "passed", + { + arguments: [], + keyword: "Given ", + line: 10, + name: "an assumption", + result: { + duration: 0, + status: "passed", + }, }, - }, - { - arguments: [], - keyword: "When ", - line: 11, - name: "a when", - result: { - duration: 0, - status: "unknown", + { + arguments: [], + keyword: "When ", + line: 11, + name: "a when", + result: { + duration: 0, + status: "unknown", + }, }, - }, - { - arguments: [], - keyword: "And ", - line: 12, - name: "an and", - result: { - duration: 0, - status: "failed", + { + arguments: [], + keyword: "And ", + line: 12, + name: "an and", + result: { + duration: 0, + status: "failed", + }, }, - }, - { - arguments: [], - keyword: "Then ", - line: 13, - name: "a then", - result: { - duration: 0, - status: "pending", + { + arguments: [], + keyword: "Then ", + line: 13, + name: "a then", + result: { + duration: 0, + status: "pending", + }, }, - }, - ], - tags: [ - { - line: 8, - name: "@ABC-63", - }, - { - line: 67, - name: "@TestName:CYP-123", - }, - ], - type: "scenario", - }, - ], - id: "a-tagged-feature", - keyword: "Feature", - line: 1, - name: "A tagged feature", - tags: [], - uri: "cypress/e2e/spec.cy.feature", - }, - ]) + ], + tags: [ + { + line: 8, + name: "@ABC-63", + }, + { + line: 67, + name: "@TestName:CYP-123", + }, + ], + type: "scenario", + }, + ], + id: "a-tagged-feature", + keyword: "Feature", + line: 1, + name: "A tagged feature", + tags: [], + uri: "cypress/e2e/spec.cy.feature", + }, + ]), + } ); const features = await command.compute(); expect(features[0].elements[0].steps[0].result.status).to.eq("undefined"); @@ -351,7 +357,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(features[0].elements).to.have.length(2); @@ -376,7 +382,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(features[0].elements[0].steps[0].embeddings).to.be.empty; @@ -406,7 +412,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(logger.message).to.have.been.calledWithExactly( @@ -484,7 +490,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(logger.message).to.have.been.calledWithExactly( @@ -550,7 +556,7 @@ describe(path.relative(process.cwd(), __filename), () => { xray: { status: {}, uploadScreenshots: false }, }, logger, - new ConstantCommand(logger, cucumberReport) + { cucumberResults: new ConstantCommand(logger, cucumberReport) } ); const features = await command.compute(); expect(features).to.have.length(1); diff --git a/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.ts b/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.ts index 77c1c253..0c9687a0 100644 --- a/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.ts +++ b/src/hooks/after/commands/conversion/cucumber/convert-cucumber-features-command.ts @@ -19,13 +19,7 @@ import { getXrayStatus } from "../util/status"; interface Parameters { cucumber: Pick; - jira: Pick< - InternalJiraOptions, - | "projectKey" - | "testExecutionIssueDescription" - | "testExecutionIssueSummary" - | "testPlanIssueKey" - >; + jira: Pick; projectRoot: string; useCloudTags?: boolean; xray: Pick; @@ -36,16 +30,18 @@ export class ConvertCucumberFeaturesCommand extends Command< Parameters > { private readonly cucumberResults: Computable; - private readonly testExecutionIssueKey?: Computable; + private readonly testExecutionIssueKey?: Computable; constructor( parameters: Parameters, logger: Logger, - cucumberResults: Computable, - testExecutionIssueKey?: Computable + input: { + cucumberResults: Computable; + testExecutionIssueKey?: Computable; + } ) { super(parameters, logger); - this.cucumberResults = cucumberResults; - this.testExecutionIssueKey = testExecutionIssueKey; + this.cucumberResults = input.cucumberResults; + this.testExecutionIssueKey = input.testExecutionIssueKey; } protected async computeResult(): Promise { diff --git a/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.spec.ts b/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.spec.ts index 20661357..2f9ef3db 100644 --- a/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.spec.ts +++ b/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.spec.ts @@ -51,7 +51,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ) as CypressRunResult_V12; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -93,7 +100,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) ) as CypressRunResult_V12; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -146,7 +160,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResult_13_0_0.json", "utf-8") ) as CypressCommandLine.CypressRunResult; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -200,7 +221,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) ) as CypressCommandLine.CypressRunResult; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -252,7 +280,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressCommandLine.CypressRunResult; result.runs[0].screenshots[0].path = "./test/resources/small.png"; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -310,7 +345,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultUnknownStatus.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -345,7 +387,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -367,20 +416,12 @@ describe(path.relative(process.cwd(), __filename), () => { mockedFs.readFileSync.onFirstCall().returns(Buffer.from("abcdef")); const command = new ConvertCypressTestsCommand( { - cucumber: { - featureFileExtension: ".feature", - }, evidenceCollection: new SimpleEvidenceCollection(), - jira: { - projectKey: "CYP", - }, - plugin: { - normalizeScreenshotNames: false, - }, - xray: { - status: {}, - uploadScreenshots: true, - }, + featureFileExtension: ".feature", + normalizeScreenshotNames: false, + projectKey: "CYP", + uploadScreenshots: true, + xrayStatus: {}, }, logger, new ConstantCommand(logger, result) @@ -401,7 +442,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.xray.uploadScreenshots = false; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -420,7 +468,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.plugin.normalizeScreenshotNames = true; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -436,7 +491,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultProblematicScreenshot.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -463,7 +525,14 @@ describe(path.relative(process.cwd(), __filename), () => { filename: "goodbye.txt", }); const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: evidenceCollection }, + { + evidenceCollection: evidenceCollection, + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -527,7 +596,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.xray.status = { passed: "it worked" }; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -543,7 +619,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.xray.status = { failed: "it did not work" }; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -558,7 +641,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.xray.status = { pending: "still pending" }; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -576,7 +666,14 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; options.xray.status = { skipped: "omit" }; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -590,7 +687,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -606,7 +710,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -622,7 +733,14 @@ describe(path.relative(process.cwd(), __filename), () => { readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( - { ...options, evidenceCollection: new SimpleEvidenceCollection() }, + { + evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, + }, logger, new ConstantCommand(logger, result) ); @@ -639,9 +757,13 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( { - ...options, evidenceCollection: new SimpleEvidenceCollection(), + featureFileExtension: options.cucumber?.featureFileExtension, + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, useCloudStatusFallback: true, + xrayStatus: options.xray.status, }, logger, new ConstantCommand(logger, result) @@ -659,10 +781,12 @@ describe(path.relative(process.cwd(), __filename), () => { ) as CypressRunResultType; const command = new ConvertCypressTestsCommand( { - ...options, - cucumber: { featureFileExtension: ".ts" }, evidenceCollection: new SimpleEvidenceCollection(), - useCloudStatusFallback: true, + featureFileExtension: ".ts", + normalizeScreenshotNames: options.plugin.normalizeScreenshotNames, + projectKey: options.jira.projectKey, + uploadScreenshots: options.xray.uploadScreenshots, + xrayStatus: options.xray.status, }, logger, new ConstantCommand(logger, result) @@ -680,20 +804,14 @@ describe(path.relative(process.cwd(), __filename), () => { const command = new ConvertCypressTestsCommand( { evidenceCollection: new SimpleEvidenceCollection(), - jira: { - projectKey: "CYP", - }, - plugin: { - normalizeScreenshotNames: true, - }, - xray: { - status: { - failed: "FAILED", - passed: "PASSED", - pending: "TODO", - skipped: "TODO", - }, - uploadScreenshots: false, + normalizeScreenshotNames: true, + projectKey: "CYP", + uploadScreenshots: false, + xrayStatus: { + failed: "FAILED", + passed: "PASSED", + pending: "TODO", + skipped: "TODO", }, }, logger, @@ -701,20 +819,14 @@ describe(path.relative(process.cwd(), __filename), () => { ); expect(command.getParameters()).to.deep.eq({ evidenceCollection: new SimpleEvidenceCollection(), - jira: { - projectKey: "CYP", - }, - plugin: { - normalizeScreenshotNames: true, - }, - xray: { - status: { - failed: "FAILED", - passed: "PASSED", - pending: "TODO", - skipped: "TODO", - }, - uploadScreenshots: false, + normalizeScreenshotNames: true, + projectKey: "CYP", + uploadScreenshots: false, + xrayStatus: { + failed: "FAILED", + passed: "PASSED", + pending: "TODO", + skipped: "TODO", }, }); }); diff --git a/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.ts b/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.ts index e0bd907a..1c52c25a 100644 --- a/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.ts +++ b/src/hooks/after/commands/conversion/cypress/convert-cypress-tests-command.ts @@ -3,12 +3,7 @@ import { lt } from "semver"; import { EvidenceCollection } from "../../../../../context"; import { RunResult as RunResult_V12 } from "../../../../../types/cypress/12.0.0/api"; import { CypressRunResultType } from "../../../../../types/cypress/cypress"; -import { - InternalCucumberOptions, - InternalJiraOptions, - InternalPluginOptions, - InternalXrayOptions, -} from "../../../../../types/plugin"; +import { InternalXrayOptions } from "../../../../../types/plugin"; import { XrayEvidenceItem, XrayTest, @@ -25,12 +20,13 @@ import { TestRunData, getTestRunData_V12, getTestRunData_V13 } from "./util/run" import { getXrayStatus } from "./util/status"; interface Parameters { - cucumber?: Pick; evidenceCollection: EvidenceCollection; - jira: Pick; - plugin: Pick; + featureFileExtension?: string; + normalizeScreenshotNames: boolean; + projectKey: string; + uploadScreenshots: boolean; useCloudStatusFallback?: boolean; - xray: Pick; + xrayStatus: InternalXrayOptions["status"]; } export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[]], Parameters> { @@ -47,7 +43,7 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] const xrayTests: XrayTest[] = []; testRunData.forEach((testData: TestRunData) => { try { - const issueKeys = getTestIssueKeys(testData.title, this.parameters.jira.projectKey); + const issueKeys = getTestIssueKeys(testData.title, this.parameters.projectKey); for (const issueKey of issueKeys) { const test: XrayTest = this.getTest( testData, @@ -87,8 +83,8 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] const conversionPromises: [string, Promise][] = []; const cypressRuns = runResults.runs.filter((run) => { return ( - !this.parameters.cucumber || - !run.spec.relative.endsWith(this.parameters.cucumber.featureFileExtension) + !this.parameters.featureFileExtension || + !run.spec.relative.endsWith(this.parameters.featureFileExtension) ); }); if (cypressRuns.length === 0) { @@ -102,7 +98,7 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] } } else { for (const run of cypressRuns as CypressCommandLine.RunResult[]) { - getTestRunData_V13(run, this.parameters.jira.projectKey).forEach((promise, index) => + getTestRunData_V13(run, this.parameters.projectKey).forEach((promise, index) => conversionPromises.push([run.tests[index].title.join(" "), promise]) ); } @@ -126,11 +122,11 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] ); } }); - if (this.parameters.xray.uploadScreenshots && version === ">=13") { + if (this.parameters.uploadScreenshots && version === ">=13") { for (const run of runResults.runs as CypressCommandLine.RunResult[]) { if ( - this.parameters.cucumber?.featureFileExtension && - run.spec.fileExtension.endsWith(this.parameters.cucumber.featureFileExtension) + this.parameters.featureFileExtension && + run.spec.fileExtension.endsWith(this.parameters.featureFileExtension) ) { continue; } @@ -146,7 +142,7 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] To upload screenshots, include test issue keys anywhere in their name: - cy.screenshot("${this.parameters.jira.projectKey}-123 ${screenshotName}") + cy.screenshot("${this.parameters.projectKey}-123 ${screenshotName}") `) ); } @@ -166,7 +162,7 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] status: getXrayStatus( test.status, this.parameters.useCloudStatusFallback === true, - this.parameters.xray.status + this.parameters.xrayStatus ), testKey: issueKey, }; @@ -182,13 +178,13 @@ export class ConvertCypressTestsCommand extends Command<[XrayTest, ...XrayTest[] version: "<13" | ">=13" ): XrayEvidenceItem[] { const evidence: XrayEvidenceItem[] = []; - if (this.parameters.xray.uploadScreenshots) { + if (this.parameters.uploadScreenshots) { for (const screenshot of testRunData.screenshots) { let filename = path.basename(screenshot.filepath); if (version === ">=13" && !filename.includes(issueKey)) { continue; } - if (this.parameters.plugin.normalizeScreenshotNames) { + if (this.parameters.normalizeScreenshotNames) { filename = normalizedFilename(filename); } evidence.push({ diff --git a/src/hooks/after/commands/conversion/util/multipart-info.spec.ts b/src/hooks/after/commands/conversion/util/multipart-info.spec.ts index 943ddf50..0fa9ac6d 100644 --- a/src/hooks/after/commands/conversion/util/multipart-info.spec.ts +++ b/src/hooks/after/commands/conversion/util/multipart-info.spec.ts @@ -16,6 +16,7 @@ describe(path.relative(process.cwd(), __filename), () => { }, { projectKey: "CYP", + testExecutionIssue: {}, } ); expect(info.fields.project).to.deep.eq({ @@ -27,8 +28,7 @@ describe(path.relative(process.cwd(), __filename), () => { Browser: Chromium (1.2.3) `) ); - expect(info.fields.summary).to.eq("Execution Results [1695916296000]"); - expect(info.fields.issuetype).to.deep.eq({ name: "Test Execution" }); + expect(info.fields.issuetype).to.be.undefined; }); it("uses provided summaries", () => { @@ -41,9 +41,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", - summary: "Hello", + testExecutionIssue: { fields: { summary: "Hello" } }, } ); expect(info.fields.summary).to.eq("Hello"); @@ -59,9 +58,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - description: "Hello There", - issuetype: {}, projectKey: "CYP", + testExecutionIssue: { fields: { description: "Hello There" } }, } ); expect(info.fields.description).to.eq("Hello There"); @@ -77,10 +75,15 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: { - name: "Test Execution (QA)", - }, projectKey: "CYP", + testExecutionIssue: { + fields: { + issuetype: { + name: "Test Execution (QA)", + }, + projectKey: "CYP", + }, + }, } ); expect(info.fields.issuetype).to.deep.eq({ @@ -98,8 +101,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", + testExecutionIssue: {}, testPlan: { value: "CYP-123", }, @@ -118,11 +121,11 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", testEnvironments: { value: ["DEV", "TEST"], }, + testExecutionIssue: {}, } ); expect(info.xrayFields).to.deep.eq({ @@ -141,14 +144,14 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28T15:51:36.000Z", }, { - custom: { + projectKey: "CYP", + testExecutionIssue: { fields: { ["customfield_12345"]: [1, 2, 3, 4, 5] }, historyMetadata: { actor: { displayName: "Jeff" } }, properties: [{ key: "???", value: "???" }], transition: { id: "15" }, update: { assignee: [{ edit: "Jeff" }] }, }, - projectKey: "CYP", } ); expect(info).to.deep.eq({ @@ -158,13 +161,11 @@ describe(path.relative(process.cwd(), __filename), () => { Cypress version: 13.2.0 Browser: Chromium (1.2.3) `), - issuetype: { - name: "Test Execution", - }, + issuetype: undefined, project: { key: "CYP", }, - summary: "Execution Results [1695916296000]", + summary: undefined, }, historyMetadata: { actor: { displayName: "Jeff" } }, properties: [{ key: "???", value: "???" }], @@ -187,7 +188,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - custom: { + projectKey: "CYP", + testExecutionIssue: { fields: { description: "My description", issuetype: { name: "Different Issue Type" }, @@ -195,7 +197,6 @@ describe(path.relative(process.cwd(), __filename), () => { summary: "My summary", }, }, - projectKey: "CYP", } ); expect(info.fields).to.deep.eq({ @@ -219,6 +220,7 @@ describe(path.relative(process.cwd(), __filename), () => { }, { projectKey: "CYPLUG", + testExecutionIssue: {}, } ); expect(info.fields.project).to.deep.eq({ @@ -230,8 +232,8 @@ describe(path.relative(process.cwd(), __filename), () => { Browser: Chromium (1.2.3) `) ); - expect(info.fields.summary).to.eq("Execution Results [1695916296000]"); - expect(info.fields.issuetype).to.deep.eq({ name: "Test Execution" }); + expect(info.fields.summary).to.be.undefined; + expect(info.fields.issuetype).to.be.undefined; }); it("uses provided summaries", () => { @@ -244,9 +246,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", - summary: "Hello", + testExecutionIssue: { fields: { summary: "Hello" } }, } ); expect(info.fields.summary).to.eq("Hello"); @@ -262,9 +263,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - description: "Hello There", - issuetype: {}, projectKey: "CYP", + testExecutionIssue: { fields: { description: "Hello There" } }, } ); expect(info.fields.description).to.eq("Hello There"); @@ -280,10 +280,14 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: { - name: "Test Execution (QA)", - }, projectKey: "CYP", + testExecutionIssue: { + fields: { + issuetype: { + name: "Test Execution (QA)", + }, + }, + }, } ); expect(info.fields.issuetype).to.deep.eq({ @@ -301,8 +305,8 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", + testExecutionIssue: {}, testPlan: { fieldId: "customField_12345", value: "CYP-123", @@ -322,12 +326,12 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - issuetype: {}, projectKey: "CYP", testEnvironments: { fieldId: "customField_12345", value: ["DEV"], }, + testExecutionIssue: {}, } ); expect(info.fields.customField_12345).to.deep.eq(["DEV"]); @@ -343,14 +347,14 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28T15:51:36.000Z", }, { - custom: { + projectKey: "CYP", + testExecutionIssue: { fields: { ["customfield_12345"]: [1, 2, 3, 4, 5] }, historyMetadata: { actor: { displayName: "Jeff" } }, properties: [{ key: "???", value: "???" }], transition: { id: "15" }, update: { assignee: [{ edit: "Jeff" }] }, }, - projectKey: "CYP", } ); expect(info).to.deep.eq({ @@ -360,13 +364,11 @@ describe(path.relative(process.cwd(), __filename), () => { Cypress version: 13.2.0 Browser: Chromium (1.2.3) `), - issuetype: { - name: "Test Execution", - }, + issuetype: undefined, project: { key: "CYP", }, - summary: "Execution Results [1695916296000]", + summary: undefined, }, historyMetadata: { actor: { displayName: "Jeff" } }, properties: [{ key: "???", value: "???" }], @@ -385,7 +387,9 @@ describe(path.relative(process.cwd(), __filename), () => { startedTestsAt: "2023-09-28 17:51:36", }, { - custom: { + projectKey: "CYP", + testEnvironments: { fieldId: "customfield_678", value: ["DEV", "TEST"] }, + testExecutionIssue: { fields: { ["customfield_678"]: ["PROD"], ["customfield_999"]: "CYP-111", @@ -395,8 +399,6 @@ describe(path.relative(process.cwd(), __filename), () => { summary: "My summary", }, }, - projectKey: "CYP", - testEnvironments: { fieldId: "customfield_678", value: ["DEV", "TEST"] }, testPlan: { fieldId: "customfield_999", value: "CYP-456" }, } ); diff --git a/src/hooks/after/commands/conversion/util/multipart-info.ts b/src/hooks/after/commands/conversion/util/multipart-info.ts index bc011cb2..6e6d86f1 100644 --- a/src/hooks/after/commands/conversion/util/multipart-info.ts +++ b/src/hooks/after/commands/conversion/util/multipart-info.ts @@ -1,5 +1,4 @@ import { CypressRunResultType } from "../../../../../types/cypress/cypress"; -import { IssueTypeDetails } from "../../../../../types/jira/responses/issue-type-details"; import { IssueUpdate } from "../../../../../types/jira/responses/issue-update"; import { MultipartInfo, @@ -19,15 +18,11 @@ export type RunData = Pick< * Additional information used by test execution issues when uploading Cucumber results. */ export interface TestExecutionIssueData { - custom?: IssueUpdate; - description?: string; - issuetype?: IssueTypeDetails; - labels?: string[]; projectKey: string; - summary?: string; testEnvironments?: { value: [string, ...string[]]; }; + testExecutionIssue: IssueUpdate; testPlan?: { value: string; }; @@ -69,11 +64,10 @@ export function buildMultipartInfoServer( multipartInfo.fields[testExecutionIssueData.testEnvironments.fieldId] = testExecutionIssueData.testEnvironments.value; } - if (testExecutionIssueData.custom?.fields) { - for (const [key, value] of Object.entries(testExecutionIssueData.custom.fields)) { - multipartInfo.fields[key] = value; - } - } + multipartInfo.fields = { + ...multipartInfo.fields, + ...testExecutionIssueData.testExecutionIssue.fields, + }; return multipartInfo; } @@ -96,11 +90,10 @@ export function buildMultipartInfoCloud( testPlanKey: testExecutionIssueData.testPlan?.value, }, }; - if (testExecutionIssueData.custom?.fields) { - for (const [key, value] of Object.entries(testExecutionIssueData.custom.fields)) { - multipartInfo.fields[key] = value; - } - } + multipartInfo.fields = { + ...multipartInfo.fields, + ...testExecutionIssueData.testExecutionIssue.fields, + }; return multipartInfo; } @@ -111,40 +104,20 @@ function getBaseInfo( return { fields: { description: - testExecutionIssueData.description ?? - defaultDescription( - runData.cypressVersion, - runData.browserName, - runData.browserVersion - ), - issuetype: testExecutionIssueData.issuetype ?? { - name: "Test Execution", - }, + testExecutionIssueData.testExecutionIssue.fields?.description ?? + dedent(` + Cypress version: ${runData.cypressVersion} + Browser: ${runData.browserName} (${runData.browserVersion}) + `), + issuetype: testExecutionIssueData.testExecutionIssue.fields?.issuetype, project: { key: testExecutionIssueData.projectKey, }, - summary: - testExecutionIssueData.summary ?? - defaultSummary(new Date(runData.startedTestsAt).getTime()), + summary: testExecutionIssueData.testExecutionIssue.fields?.summary, }, - historyMetadata: testExecutionIssueData.custom?.historyMetadata, - properties: testExecutionIssueData.custom?.properties, - transition: testExecutionIssueData.custom?.transition, - update: testExecutionIssueData.custom?.update, + historyMetadata: testExecutionIssueData.testExecutionIssue.historyMetadata, + properties: testExecutionIssueData.testExecutionIssue.properties, + transition: testExecutionIssueData.testExecutionIssue.transition, + update: testExecutionIssueData.testExecutionIssue.update, }; } - -function defaultSummary(timestamp: number): string { - return `Execution Results [${timestamp.toString()}]`; -} - -function defaultDescription( - cypressVersion: string, - browserName: string, - browserVersion: string -): string { - return dedent(` - Cypress version: ${cypressVersion} - Browser: ${browserName} (${browserVersion}) - `); -} diff --git a/src/hooks/after/commands/extract-execution-issue-type-command.spec.ts b/src/hooks/after/commands/extract-execution-issue-type-command.spec.ts deleted file mode 100644 index e714a74e..00000000 --- a/src/hooks/after/commands/extract-execution-issue-type-command.spec.ts +++ /dev/null @@ -1,331 +0,0 @@ -import chai, { expect } from "chai"; -import chaiAsPromised from "chai-as-promised"; -import fs from "fs"; -import path from "path"; -import { getMockedLogger } from "../../../../test/mocks"; -import { IssueTypeDetails } from "../../../types/jira/responses/issue-type-details"; -import { dedent } from "../../../util/dedent"; -import { ConstantCommand } from "../../util/commands/constant-command"; -import { ExtractExecutionIssueTypeCommand } from "./extract-execution-issue-type-command"; - -chai.use(chaiAsPromised); - -describe(path.relative(process.cwd(), __filename), () => { - describe(ExtractExecutionIssueTypeCommand.name, () => { - it("extracts test execution issue types", async () => { - const logger = getMockedLogger(); - const issueTypes = JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getIssueTypes.json", - "utf-8" - ) - ) as IssueTypeDetails[]; - const command = new ExtractExecutionIssueTypeCommand( - { - displayCloudHelp: true, - projectKey: "CYP", - testExecutionIssueType: { name: "Test Execution" }, - }, - logger, - new ConstantCommand(logger, issueTypes) - ); - expect(await command.compute()).to.deep.eq({ - avatarId: 10515, - description: - "This is the Xray Test Execution Issue Type. Used to execute test cases already defined.", - hierarchyLevel: 0, - iconUrl: - "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10515?size=medium", - id: "10008", - name: "Test Execution", - self: "https://example.org/rest/api/2/issuetype/10008", - subtask: false, - untranslatedName: "Test Execution", - }); - }); - - it("throws when the test execution types do not exist (cloud)", async () => { - const logger = getMockedLogger(); - const issueTypes = JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getIssueTypes.json", - "utf-8" - ) - ) as IssueTypeDetails[]; - const command = new ExtractExecutionIssueTypeCommand( - { - displayCloudHelp: true, - projectKey: "CYP", - testExecutionIssueType: { name: "Nonexistent Execution" }, - }, - logger, - new ConstantCommand(logger, issueTypes) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: { - "name": "Nonexistent Execution" - } - - Make sure Xray's issue types have been added to project CYP or that you've configured any custom execution issue type accordingly - - For example, the following plugin configuration will tell Xray to create Jira issues of type "My Custom Issue Type" to document test execution results: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#issuetype - - https://docs.getxray.app/display/XRAYCLOUD/Project+Settings%3A+Issue+Types+Mapping - `) - ); - }); - - it("throws when the test execution types do not exist (server)", async () => { - const logger = getMockedLogger(); - const issueTypes = JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getIssueTypes.json", - "utf-8" - ) - ) as IssueTypeDetails[]; - const command = new ExtractExecutionIssueTypeCommand( - { - displayCloudHelp: false, - projectKey: "CYP", - testExecutionIssueType: { name: "Nonexistent Execution" }, - }, - logger, - new ConstantCommand(logger, issueTypes) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: { - "name": "Nonexistent Execution" - } - - Make sure Xray's issue types have been added to project CYP or that you've configured any custom execution issue type accordingly - - For example, the following plugin configuration will tell Xray to create Jira issues of type "My Custom Issue Type" to document test execution results: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#issuetype - - https://docs.getxray.app/display/XRAY/Configuring+a+Jira+project+to+be+used+as+an+Xray+Test+Project - `) - ); - }); - - it("throws when multiple test execution types exist (cloud)", async () => { - const logger = getMockedLogger(); - const issueTypes = JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getIssueTypes.json", - "utf-8" - ) - ) as IssueTypeDetails[]; - const command = new ExtractExecutionIssueTypeCommand( - { - displayCloudHelp: true, - projectKey: "CYP", - testExecutionIssueType: { name: "Task" }, - }, - logger, - new ConstantCommand(logger, issueTypes) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: { - "name": "Task" - } - - There are multiple issue types with this name, make sure to only make a single one available in project CYP: - - { - "self": "https://example.org/rest/api/2/issuetype/10002", - "id": "10002", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0 - } - - { - "self": "https://example.org/rest/api/2/issuetype/10014", - "id": "10014", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0, - "scope": { - "type": "PROJECT", - "project": { - "id": "10008" - } - } - } - - { - "self": "https://example.org/rest/api/2/issuetype/10018", - "id": "10018", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0, - "scope": { - "type": "PROJECT", - "project": { - "id": "10009" - } - } - } - - If none of them is the test execution issue type you're using in project CYP, please specify the correct text execution issue type in the plugin configuration: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#issuetype - - https://docs.getxray.app/display/XRAYCLOUD/Project+Settings%3A+Issue+Types+Mapping - `) - ); - }); - - it("throws when multiple test execution types exist (server)", async () => { - const logger = getMockedLogger(); - const issueTypes = JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getIssueTypes.json", - "utf-8" - ) - ) as IssueTypeDetails[]; - const command = new ExtractExecutionIssueTypeCommand( - { - displayCloudHelp: false, - projectKey: "CYP", - testExecutionIssueType: { name: "Task" }, - }, - logger, - new ConstantCommand(logger, issueTypes) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: { - "name": "Task" - } - - There are multiple issue types with this name, make sure to only make a single one available in project CYP: - - { - "self": "https://example.org/rest/api/2/issuetype/10002", - "id": "10002", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0 - } - - { - "self": "https://example.org/rest/api/2/issuetype/10014", - "id": "10014", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0, - "scope": { - "type": "PROJECT", - "project": { - "id": "10008" - } - } - } - - { - "self": "https://example.org/rest/api/2/issuetype/10018", - "id": "10018", - "description": "Ein kleine, bestimmte Aufgabe.", - "iconUrl": "https://example.org/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium", - "name": "Task", - "untranslatedName": "Task", - "subtask": false, - "avatarId": 10318, - "hierarchyLevel": 0, - "scope": { - "type": "PROJECT", - "project": { - "id": "10009" - } - } - } - - If none of them is the test execution issue type you're using in project CYP, please specify the correct text execution issue type in the plugin configuration: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#issuetype - - https://docs.getxray.app/display/XRAY/Configuring+a+Jira+project+to+be+used+as+an+Xray+Test+Project - `) - ); - }); - }); -}); diff --git a/src/hooks/after/commands/extract-execution-issue-type-command.ts b/src/hooks/after/commands/extract-execution-issue-type-command.ts deleted file mode 100644 index 8d0cecba..00000000 --- a/src/hooks/after/commands/extract-execution-issue-type-command.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { IssueTypeDetails } from "../../../types/jira/responses/issue-type-details"; -import { InternalJiraOptions } from "../../../types/plugin"; -import { contains } from "../../../util/compare"; -import { dedent } from "../../../util/dedent"; -import { HELP } from "../../../util/help"; -import { Logger } from "../../../util/logging"; -import { Command, Computable } from "../../command"; - -type Parameters = Pick & { - displayCloudHelp: boolean; - testExecutionIssueType: IssueTypeDetails; -}; - -export class ExtractExecutionIssueTypeCommand extends Command { - private readonly allIssueDetails: Computable; - - constructor( - parameters: Parameters, - logger: Logger, - allIssueDetails: Computable - ) { - super(parameters, logger); - this.allIssueDetails = allIssueDetails; - } - - protected async computeResult(): Promise { - const allIssueDetails = await this.allIssueDetails.compute(); - const executionIssueDetails = allIssueDetails.filter((details: IssueTypeDetails) => - contains(details, this.parameters.testExecutionIssueType) - ); - if (executionIssueDetails.length === 0) { - throw new Error( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: ${JSON.stringify( - this.parameters.testExecutionIssueType, - null, - 2 - )} - - Make sure Xray's issue types have been added to project ${ - this.parameters.projectKey - } or that you've configured any custom execution issue type accordingly - - For example, the following plugin configuration will tell Xray to create Jira issues of type "My Custom Issue Type" to document test execution results: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - ${HELP.plugin.configuration.jira.testExecutionIssue.fields.issuetype} - - ${ - this.parameters.displayCloudHelp - ? HELP.xray.issueTypeMapping.cloud - : HELP.xray.issueTypeMapping.server - } - `) - ); - } else if (executionIssueDetails.length > 1) { - throw new Error( - dedent(` - Failed to retrieve Jira issue type information of test execution issue type: ${JSON.stringify( - this.parameters.testExecutionIssueType, - null, - 2 - )} - - There are multiple issue types with this name, make sure to only make a single one available in project ${ - this.parameters.projectKey - }: - - ${executionIssueDetails - .map((details) => JSON.stringify(details, null, 2)) - .join("\n\n")} - - If none of them is the test execution issue type you're using in project ${ - this.parameters.projectKey - }, please specify the correct text execution issue type in the plugin configuration: - - { - jira: { - testExecutionIssue: { - fields: { - issuetype: { - name: "My Custom Issue Type" - // ... - } - } - } - } - } - - For more information, visit: - - ${HELP.plugin.configuration.jira.testExecutionIssue.fields.issuetype} - - ${ - this.parameters.displayCloudHelp - ? HELP.xray.issueTypeMapping.cloud - : HELP.xray.issueTypeMapping.server - } - `) - ); - } - return executionIssueDetails[0]; - } -} diff --git a/src/hooks/preprocessor/file-preprocessor.spec.ts b/src/hooks/preprocessor/file-preprocessor.spec.ts index b9ff2d34..9957d3f6 100644 --- a/src/hooks/preprocessor/file-preprocessor.spec.ts +++ b/src/hooks/preprocessor/file-preprocessor.spec.ts @@ -12,7 +12,6 @@ import { ClientCombination, InternalCypressXrayPluginOptions } from "../../types import { ExecutableGraph } from "../../util/graph/executable-graph"; import { Command } from "../command"; import { EditIssueFieldCommand } from "../util/commands/jira/edit-issue-field-command"; -import { JiraField } from "../util/commands/jira/extract-field-id-command"; import { GetLabelValuesCommand } from "../util/commands/jira/get-label-values-command"; import { GetSummaryValuesCommand } from "../util/commands/jira/get-summary-values-command"; import { ImportFeatureCommand } from "../util/commands/xray/import-feature-command"; @@ -107,12 +106,12 @@ describe(path.relative(process.cwd(), __filename), () => { assertIsInstanceOf(commands[10], GetLabelsToResetCommand); assertIsInstanceOf(commands[11], EditIssueFieldCommand); expect(commands[11].getParameters()).to.deep.eq({ - field: JiraField.SUMMARY, + fieldId: "summary", jiraClient: clients.jiraClient, }); assertIsInstanceOf(commands[12], EditIssueFieldCommand); expect(commands[12].getParameters()).to.deep.eq({ - field: JiraField.LABELS, + fieldId: "labels", jiraClient: clients.jiraClient, }); expect(graph.size("vertices")).to.eq(13); diff --git a/src/hooks/preprocessor/file-preprocessor.ts b/src/hooks/preprocessor/file-preprocessor.ts index 40e79158..14d9b4c5 100644 --- a/src/hooks/preprocessor/file-preprocessor.ts +++ b/src/hooks/preprocessor/file-preprocessor.ts @@ -1,6 +1,5 @@ import { Command } from "../command"; import { EditIssueFieldCommand } from "../util/commands/jira/edit-issue-field-command"; -import { JiraField } from "../util/commands/jira/extract-field-id-command"; import { GetLabelValuesCommand } from "../util/commands/jira/get-label-values-command"; import { GetSummaryValuesCommand } from "../util/commands/jira/get-summary-values-command"; import { ImportFeatureCommand } from "../util/commands/xray/import-feature-command"; @@ -64,7 +63,6 @@ export function addSynchronizationCommands( new GetLabelValuesCommand( { jiraClient: clients.jiraClient }, logger, - new ConstantCommand(logger, "labels"), extractIssueKeysCommand ) ); @@ -106,7 +104,6 @@ export function addSynchronizationCommands( new GetLabelValuesCommand( { jiraClient: clients.jiraClient }, logger, - new ConstantCommand(logger, "labels"), extractIssueKeysCommand ) ); @@ -124,7 +121,7 @@ export function addSynchronizationCommands( graph.connect(getNewLabelsCommand, getLabelsToResetCommand); const editSummariesCommand = graph.place( new EditIssueFieldCommand( - { field: JiraField.SUMMARY, jiraClient: clients.jiraClient }, + { fieldId: "summary", jiraClient: clients.jiraClient }, logger, new ConstantCommand(logger, "summary"), getSummariesToResetCommand @@ -133,7 +130,7 @@ export function addSynchronizationCommands( graph.connect(getSummariesToResetCommand, editSummariesCommand); const editLabelsCommand = graph.place( new EditIssueFieldCommand( - { field: JiraField.LABELS, jiraClient: clients.jiraClient }, + { fieldId: "labels", jiraClient: clients.jiraClient }, logger, new ConstantCommand(logger, "labels"), getLabelsToResetCommand diff --git a/src/hooks/util/commands/jira/edit-issue-field-command.spec.ts b/src/hooks/util/commands/jira/edit-issue-field-command.spec.ts index ff4f97f8..52cb3203 100644 --- a/src/hooks/util/commands/jira/edit-issue-field-command.spec.ts +++ b/src/hooks/util/commands/jira/edit-issue-field-command.spec.ts @@ -6,7 +6,6 @@ import { dedent } from "../../../../util/dedent"; import { Level } from "../../../../util/logging"; import { ConstantCommand } from "../constant-command"; import { EditIssueFieldCommand } from "./edit-issue-field-command"; -import { JiraField } from "./extract-field-id-command"; chai.use(chaiAsPromised); @@ -23,7 +22,7 @@ describe(path.relative(process.cwd(), __filename), () => { .resolves("CYP-456"); const command = new EditIssueFieldCommand( { - field: JiraField.SUMMARY, + fieldId: "summary", jiraClient: jiraClient, }, logger, @@ -48,7 +47,7 @@ describe(path.relative(process.cwd(), __filename), () => { .rejects(new Error("No editing allowed")); const command = new EditIssueFieldCommand( { - field: JiraField.LABELS, + fieldId: "labels", jiraClient: jiraClient, }, logger, @@ -74,7 +73,7 @@ describe(path.relative(process.cwd(), __filename), () => { const jiraClient = getMockedJiraClient(); const command = new EditIssueFieldCommand( { - field: JiraField.LABELS, + fieldId: "labels", jiraClient: jiraClient, }, logger, diff --git a/src/hooks/util/commands/jira/edit-issue-field-command.ts b/src/hooks/util/commands/jira/edit-issue-field-command.ts index bcc444c5..c0ac1d48 100644 --- a/src/hooks/util/commands/jira/edit-issue-field-command.ts +++ b/src/hooks/util/commands/jira/edit-issue-field-command.ts @@ -4,24 +4,20 @@ import { dedent } from "../../../../util/dedent"; import { Level, Logger } from "../../../../util/logging"; import { unknownToString } from "../../../../util/string"; import { Command, Computable } from "../../../command"; -import { FieldValueMap } from "./get-field-values-command"; -interface Parameters { - field: F; +interface Parameters { + fieldId: string; jiraClient: JiraClient; } -export class EditIssueFieldCommand extends Command< - string[], - Parameters -> { +export class EditIssueFieldCommand extends Command { private readonly fieldId: Computable; - private readonly fieldValues: Computable>; + private readonly fieldValues: Computable>; constructor( - parameters: Parameters, + parameters: Parameters, logger: Logger, fieldId: Computable, - fieldValues: Computable> + fieldValues: Computable> ) { super(parameters, logger); this.fieldId = fieldId; @@ -47,7 +43,7 @@ export class EditIssueFieldCommand extends Comman ${issueKey} Failed to set ${unknownToString( - this.parameters.field + this.parameters.fieldId )} field to value: ${unknownToString(newValue)} `) ); diff --git a/src/hooks/util/commands/jira/extract-field-id-command.spec.ts b/src/hooks/util/commands/jira/extract-field-id-command.spec.ts index 52bb45ac..a87e0018 100644 --- a/src/hooks/util/commands/jira/extract-field-id-command.spec.ts +++ b/src/hooks/util/commands/jira/extract-field-id-command.spec.ts @@ -13,18 +13,18 @@ describe(path.relative(process.cwd(), __filename), () => { it("extracts fields case-insensitively", async () => { const logger = getMockedLogger(); const command = new ExtractFieldIdCommand( - { field: JiraField.SUMMARY }, + { field: JiraField.TEST_PLAN }, logger, new ConstantCommand(logger, [ { - clauseNames: ["summary"], + clauseNames: ["test plan"], custom: false, id: "customfield_12345", - name: "Summary", + name: "Test Plan", navigable: true, orderable: true, schema: { - system: "summary", + system: "test plan", type: "string", }, searchable: true, @@ -50,7 +50,7 @@ describe(path.relative(process.cwd(), __filename), () => { it("throws for missing fields", async () => { const logger = getMockedLogger(); const command = new ExtractFieldIdCommand( - { field: JiraField.DESCRIPTION }, + { field: JiraField.TEST_PLAN }, logger, new ConstantCommand(logger, [ { @@ -70,7 +70,7 @@ describe(path.relative(process.cwd(), __filename), () => { ); await expect(command.compute()).to.eventually.be.rejectedWith( dedent(` - Failed to fetch Jira field ID for field with name: description + Failed to fetch Jira field ID for field with name: test plan Make sure the field actually exists and that your Jira language settings did not modify the field's name Available fields: @@ -80,7 +80,7 @@ describe(path.relative(process.cwd(), __filename), () => { jira: { fields: { - description: // corresponding field ID + testPlan: // corresponding field ID } } `) @@ -88,75 +88,6 @@ describe(path.relative(process.cwd(), __filename), () => { }); describe("throws for missing fields and displays a hint", () => { - it(JiraField.DESCRIPTION, async () => { - const logger = getMockedLogger(); - const command = new ExtractFieldIdCommand( - { field: JiraField.DESCRIPTION }, - logger, - new ConstantCommand(logger, []) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to fetch Jira field ID for field with name: description - Make sure the field actually exists and that your Jira language settings did not modify the field's name - - You can provide field IDs directly without relying on language settings: - - jira: { - fields: { - description: // corresponding field ID - } - } - `) - ); - }); - - it(JiraField.SUMMARY, async () => { - const logger = getMockedLogger(); - const command = new ExtractFieldIdCommand( - { field: JiraField.SUMMARY }, - logger, - new ConstantCommand(logger, []) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to fetch Jira field ID for field with name: summary - Make sure the field actually exists and that your Jira language settings did not modify the field's name - - You can provide field IDs directly without relying on language settings: - - jira: { - fields: { - summary: // corresponding field ID - } - } - `) - ); - }); - - it(JiraField.LABELS, async () => { - const logger = getMockedLogger(); - const command = new ExtractFieldIdCommand( - { field: JiraField.LABELS }, - logger, - new ConstantCommand(logger, []) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to fetch Jira field ID for field with name: labels - Make sure the field actually exists and that your Jira language settings did not modify the field's name - - You can provide field IDs directly without relying on language settings: - - jira: { - fields: { - labels: // corresponding field ID - } - } - `) - ); - }); - it(JiraField.TEST_ENVIRONMENTS, async () => { const logger = getMockedLogger(); const command = new ExtractFieldIdCommand( @@ -202,55 +133,32 @@ describe(path.relative(process.cwd(), __filename), () => { `) ); }); - - it(JiraField.TEST_TYPE, async () => { - const logger = getMockedLogger(); - const command = new ExtractFieldIdCommand( - { field: JiraField.TEST_TYPE }, - logger, - new ConstantCommand(logger, []) - ); - await expect(command.compute()).to.eventually.be.rejectedWith( - dedent(` - Failed to fetch Jira field ID for field with name: test type - Make sure the field actually exists and that your Jira language settings did not modify the field's name - - You can provide field IDs directly without relying on language settings: - - jira: { - fields: { - testType: // corresponding field ID - } - } - `) - ); - }); }); it("throws for multiple fields", async () => { const logger = getMockedLogger(); const command = new ExtractFieldIdCommand( - { field: JiraField.SUMMARY }, + { field: JiraField.TEST_PLAN }, logger, new ConstantCommand(logger, [ { - clauseNames: ["summary"], + clauseNames: ["Test Plan"], custom: false, - id: "summary", - name: "summary", + id: "testPlan", + name: "Test Plan", navigable: true, orderable: true, schema: { - system: "summary", + system: "Test Plan", type: "string", }, searchable: true, }, { - clauseNames: ["summary (custom)"], + clauseNames: ["Test Plan (custom)"], custom: false, id: "customfield_12345", - name: "Summary", + name: "Test Plan", navigable: true, orderable: true, schema: { @@ -263,18 +171,18 @@ describe(path.relative(process.cwd(), __filename), () => { ); await expect(command.compute()).to.eventually.be.rejectedWith( dedent(` - Failed to fetch Jira field ID for field with name: summary + Failed to fetch Jira field ID for field with name: test plan There are multiple fields with this name Duplicates: - clauseNames: ["summary (custom)"], custom: false, id: "customfield_12345", name: "Summary", navigable: true, orderable: true, schema: {"customId":5125,"type":"string"} , searchable: true - clauseNames: ["summary"] , custom: false, id: "summary" , name: "summary", navigable: true, orderable: true, schema: {"system":"summary","type":"string"}, searchable: true + clauseNames: ["Test Plan (custom)"], custom: false, id: "customfield_12345", name: "Test Plan", navigable: true, orderable: true, schema: {"customId":5125,"type":"string"} , searchable: true + clauseNames: ["Test Plan"] , custom: false, id: "testPlan" , name: "Test Plan", navigable: true, orderable: true, schema: {"system":"Test Plan","type":"string"}, searchable: true You can provide field IDs in the options: jira: { fields: { - summary: // "summary" or "customfield_12345" + testPlan: // "testPlan" or "customfield_12345" } } `) diff --git a/src/hooks/util/commands/jira/extract-field-id-command.ts b/src/hooks/util/commands/jira/extract-field-id-command.ts index 65dbabde..b12d9471 100644 --- a/src/hooks/util/commands/jira/extract-field-id-command.ts +++ b/src/hooks/util/commands/jira/extract-field-id-command.ts @@ -7,12 +7,8 @@ import { prettyPadObjects, prettyPadValues } from "../../../../util/pretty"; import { Command, Computable } from "../../../command"; export enum JiraField { - DESCRIPTION = "description", - LABELS = "labels", - SUMMARY = "summary", TEST_ENVIRONMENTS = "test environments", TEST_PLAN = "test plan", - TEST_TYPE = "test type", } interface Parameters { @@ -111,17 +107,9 @@ export class ExtractFieldIdCommand extends Command { function getOptionName(fieldName: JiraField): keyof JiraFieldIds { switch (fieldName) { - case JiraField.DESCRIPTION: - return "description"; - case JiraField.SUMMARY: - return "summary"; - case JiraField.LABELS: - return "labels"; case JiraField.TEST_ENVIRONMENTS: return "testEnvironments"; case JiraField.TEST_PLAN: return "testPlan"; - case JiraField.TEST_TYPE: - return "testType"; } } diff --git a/src/hooks/util/commands/jira/get-field-values-command.ts b/src/hooks/util/commands/jira/get-field-values-command.ts index 1f18d2a2..0507f44b 100644 --- a/src/hooks/util/commands/jira/get-field-values-command.ts +++ b/src/hooks/util/commands/jira/get-field-values-command.ts @@ -5,20 +5,13 @@ import { dedent } from "../../../../util/dedent"; import { errorMessage } from "../../../../util/errors"; import { Level, Logger } from "../../../../util/logging"; import { Command, Computable } from "../../../command"; -import { JiraField } from "./extract-field-id-command"; - -export interface FieldValueMap { - [JiraField.LABELS]: string[]; - [JiraField.SUMMARY]: string; - [JiraField.TEST_TYPE]: string; -} interface Parameters { jiraClient: JiraClient; } -export abstract class GetFieldValuesCommand extends Command< - StringMap, +export abstract class GetFieldValuesCommand extends Command< + StringMap, Parameters > { protected readonly fieldId: Computable; @@ -35,8 +28,8 @@ export abstract class GetFieldValuesCommand exten } protected async extractJiraFieldValues( - extractor: (issue: Issue, fieldId: string) => FieldValueMap[F] | Promise - ): Promise> { + extractor: (issue: Issue, fieldId: string) => FieldValue | Promise + ): Promise> { const fieldId = await this.fieldId.compute(); const issueKeys = await this.issueKeys.compute(); const issues: Issue[] = await this.parameters.jiraClient.search({ @@ -55,7 +48,7 @@ export abstract class GetFieldValuesCommand exten `) ); } - const results: StringMap = {}; + const results: StringMap = {}; const issuesWithUnparseableField: string[] = []; for (const issue of issues) { if (!issue.key) { diff --git a/src/hooks/util/commands/jira/get-label-values-command.spec.ts b/src/hooks/util/commands/jira/get-label-values-command.spec.ts index 31c0b2d0..572bb35d 100644 --- a/src/hooks/util/commands/jira/get-label-values-command.spec.ts +++ b/src/hooks/util/commands/jira/get-label-values-command.spec.ts @@ -17,18 +17,17 @@ describe(path.relative(process.cwd(), __filename), () => { const command = new GetLabelValuesCommand( { jiraClient: jiraClient }, logger, - new ConstantCommand(logger, "labelId"), new ConstantCommand(logger, ["CYP-123", "CYP-456", "CYP-789"]) ); jiraClient.search .withArgs({ - fields: ["labelId"], + fields: ["labels"], jql: "issue in (CYP-123,CYP-456,CYP-789)", }) .resolves([ - { fields: { labelId: ["label", "two labels"] }, key: "CYP-123" }, - { fields: { labelId: ["three labels"] }, key: "CYP-456" }, - { fields: { labelId: [] }, key: "CYP-789" }, + { fields: { labels: ["label", "two labels"] }, key: "CYP-123" }, + { fields: { labels: ["three labels"] }, key: "CYP-456" }, + { fields: { labels: [] }, key: "CYP-789" }, ]); const summaries = await command.compute(); expect(summaries).to.deep.eq({ @@ -44,15 +43,14 @@ describe(path.relative(process.cwd(), __filename), () => { const command = new GetLabelValuesCommand( { jiraClient: jiraClient }, logger, - new ConstantCommand(logger, "labelId"), new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) ); jiraClient.search .withArgs({ - fields: ["labelId"], + fields: ["labels"], jql: "issue in (CYP-123,CYP-789,CYP-456)", }) - .resolves([{ fields: { labelId: ["label"] }, key: "CYP-123" }]); + .resolves([{ fields: { labels: ["label"] }, key: "CYP-123" }]); expect(await command.compute()).to.deep.eq({ ["CYP-123"]: ["label"], }); @@ -73,30 +71,29 @@ describe(path.relative(process.cwd(), __filename), () => { const command = new GetLabelValuesCommand( { jiraClient: jiraClient }, logger, - new ConstantCommand(logger, "labelId"), new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) ); jiraClient.search .withArgs({ - fields: ["labelId"], + fields: ["labels"], jql: "issue in (CYP-123,CYP-789,CYP-456)", }) .resolves([ - { fields: { labelId: "string" }, key: "CYP-123" }, + { fields: { labels: "string" }, key: "CYP-123" }, { fields: { bonjour: 42 }, key: "CYP-456" }, - { fields: { labelId: [42, 84] }, key: "CYP-789" }, - { fields: { labelId: ["hi", "there"] } }, + { fields: { labels: [42, 84] }, key: "CYP-789" }, + { fields: { labels: ["hi", "there"] } }, ]); expect(await command.compute()).to.deep.eq({}); expect(logger.message).to.have.been.calledWithExactly( Level.WARNING, dedent(` - Failed to parse Jira field with ID labelId in issues: + Failed to parse Jira field with ID labels in issues: CYP-123: Value is not an array of type string: "string" - CYP-456: Expected an object containing property 'labelId', but got: {"bonjour":42} + CYP-456: Expected an object containing property 'labels', but got: {"bonjour":42} CYP-789: Value is not an array of type string: [42,84] - Unknown: {"fields":{"labelId":["hi","there"]}} + Unknown: {"fields":{"labels":["hi","there"]}} `) ); }); @@ -107,12 +104,11 @@ describe(path.relative(process.cwd(), __filename), () => { const command = new GetLabelValuesCommand( { jiraClient: jiraClient }, logger, - new ConstantCommand(logger, "labelId"), new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) ); jiraClient.search .withArgs({ - fields: ["labelId"], + fields: ["labels"], jql: "issue in (CYP-123,CYP-789,CYP-456)", }) .rejects(new Error("Connection timeout")); diff --git a/src/hooks/util/commands/jira/get-label-values-command.ts b/src/hooks/util/commands/jira/get-label-values-command.ts index 1bb0a0f2..0cd0cd3b 100644 --- a/src/hooks/util/commands/jira/get-label-values-command.ts +++ b/src/hooks/util/commands/jira/get-label-values-command.ts @@ -1,15 +1,26 @@ +import { JiraClient } from "../../../../client/jira/jira-client"; import { Issue } from "../../../../types/jira/responses/issue"; import { StringMap } from "../../../../types/util"; import { extractArrayOfStrings } from "../../../../util/extraction"; -import { JiraField } from "./extract-field-id-command"; +import { Logger } from "../../../../util/logging"; +import { Computable } from "../../../command"; +import { ConstantCommand } from "../constant-command"; import { GetFieldValuesCommand } from "./get-field-values-command"; -export class GetLabelValuesCommand extends GetFieldValuesCommand { +export class GetLabelValuesCommand extends GetFieldValuesCommand { + constructor( + parameters: { jiraClient: JiraClient }, + logger: Logger, + issueKeys: Computable + ) { + super(parameters, logger, new ConstantCommand(logger, "labels"), issueKeys); + } + protected async computeResult(): Promise> { // Field property example: // labels: ["regression", "quality"] - return await this.extractJiraFieldValues((issue: Issue, fieldId: string) => - extractArrayOfStrings(issue.fields, fieldId) + return await this.extractJiraFieldValues((issue: Issue) => + extractArrayOfStrings(issue.fields, "labels") ); } } diff --git a/src/hooks/util/commands/jira/get-summary-values-command.ts b/src/hooks/util/commands/jira/get-summary-values-command.ts index a4898e88..6fcdb28e 100644 --- a/src/hooks/util/commands/jira/get-summary-values-command.ts +++ b/src/hooks/util/commands/jira/get-summary-values-command.ts @@ -5,10 +5,9 @@ import { extractString } from "../../../../util/extraction"; import { Logger } from "../../../../util/logging"; import { Computable } from "../../../command"; import { ConstantCommand } from "../constant-command"; -import { JiraField } from "./extract-field-id-command"; import { GetFieldValuesCommand } from "./get-field-values-command"; -export class GetSummaryValuesCommand extends GetFieldValuesCommand { +export class GetSummaryValuesCommand extends GetFieldValuesCommand { constructor( parameters: { jiraClient: JiraClient }, logger: Logger, diff --git a/src/hooks/util/commands/jira/get-test-type-values-command.spec.ts b/src/hooks/util/commands/jira/get-test-type-values-command.spec.ts deleted file mode 100644 index aa6d7f01..00000000 --- a/src/hooks/util/commands/jira/get-test-type-values-command.spec.ts +++ /dev/null @@ -1,196 +0,0 @@ -import chai, { expect } from "chai"; -import chaiAsPromised from "chai-as-promised"; -import path from "path"; -import { - getMockedJiraClient, - getMockedLogger, - getMockedXrayClient, -} from "../../../../../test/mocks"; -import { dedent } from "../../../../util/dedent"; -import { Level } from "../../../../util/logging"; -import { ConstantCommand } from "../constant-command"; -import { - GetTestTypeValuesCommandCloud, - GetTestTypeValuesCommandServer, -} from "./get-test-type-values-command"; - -chai.use(chaiAsPromised); - -describe(path.relative(process.cwd(), __filename), () => { - describe(GetTestTypeValuesCommandServer.name, () => { - it("fetches test types", async () => { - const logger = getMockedLogger(); - const jiraClient = getMockedJiraClient(); - const command = new GetTestTypeValuesCommandServer( - { jiraClient: jiraClient }, - logger, - new ConstantCommand(logger, "customfield_12345"), - new ConstantCommand(logger, ["CYP-123", "CYP-456", "CYP-789"]) - ); - jiraClient.search - .withArgs({ - fields: ["customfield_12345"], - jql: "issue in (CYP-123,CYP-456,CYP-789)", - }) - .resolves([ - { fields: { ["customfield_12345"]: { value: "Cucumber" } }, key: "CYP-123" }, - { fields: { ["customfield_12345"]: { value: "Generic" } }, key: "CYP-456" }, - { fields: { ["customfield_12345"]: { value: "Manual" } }, key: "CYP-789" }, - ]); - const summaries = await command.compute(); - expect(summaries).to.deep.eq({ - ["CYP-123"]: "Cucumber", - ["CYP-456"]: "Generic", - ["CYP-789"]: "Manual", - }); - }); - - it("displays a warning for issues which do not exist", async () => { - const logger = getMockedLogger(); - const jiraClient = getMockedJiraClient(); - const command = new GetTestTypeValuesCommandServer( - { jiraClient: jiraClient }, - logger, - new ConstantCommand(logger, "customfield_12345"), - new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) - ); - jiraClient.search - .withArgs({ - fields: ["customfield_12345"], - jql: "issue in (CYP-123,CYP-789,CYP-456)", - }) - .resolves([ - { fields: { ["customfield_12345"]: { value: "Cucumber" } }, key: "CYP-123" }, - ]); - expect(await command.compute()).to.deep.eq({ - ["CYP-123"]: "Cucumber", - }); - expect(logger.message).to.have.been.calledWithExactly( - Level.WARNING, - dedent(` - Failed to find Jira issues: - - CYP-456 - CYP-789 - `) - ); - }); - - it("displays a warning for issues whose fields cannot be parsed", async () => { - const logger = getMockedLogger(); - const jiraClient = getMockedJiraClient(); - const command = new GetTestTypeValuesCommandServer( - { jiraClient: jiraClient }, - logger, - new ConstantCommand(logger, "customfield_12345"), - new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) - ); - jiraClient.search - .withArgs({ - fields: ["customfield_12345"], - jql: "issue in (CYP-123,CYP-789,CYP-456)", - }) - .resolves([ - { fields: { ["customfield_12345"]: { an: "object" } }, key: "CYP-123" }, - { - fields: { bonjour: ["Where", "did", "I", "come", "from?"] }, - key: "CYP-456", - }, - { fields: { ["customfield_12345"]: [42, 84] }, key: "CYP-789" }, - { fields: { ["customfield_12345"]: { value: "Manual" } } }, - ]); - expect(await command.compute()).to.deep.eq({}); - expect(logger.message).to.have.been.calledWithExactly( - Level.WARNING, - dedent(` - Failed to parse Jira field with ID customfield_12345 in issues: - - CYP-123: Expected an object containing property 'value', but got: {"an":"object"} - CYP-456: Expected an object containing property 'customfield_12345', but got: {"bonjour":["Where","did","I","come","from?"]} - CYP-789: Expected an object containing property 'value', but got: [42,84] - Unknown: {"fields":{"customfield_12345":{"value":"Manual"}}} - `) - ); - }); - - it("throws when encountering search failures", async () => { - const logger = getMockedLogger(); - const jiraClient = getMockedJiraClient(); - const command = new GetTestTypeValuesCommandServer( - { jiraClient: jiraClient }, - logger, - new ConstantCommand(logger, "customfield_12345"), - new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) - ); - jiraClient.search - .withArgs({ - fields: ["customfield_12345"], - jql: "issue in (CYP-123,CYP-789,CYP-456)", - }) - .rejects(new Error("Connection timeout")); - await expect(command.compute()).to.eventually.be.rejectedWith("Connection timeout"); - }); - }); - - describe(GetTestTypeValuesCommandCloud.name, () => { - it("fetches test types", async () => { - const logger = getMockedLogger(); - const xrayClient = getMockedXrayClient("cloud"); - const command = new GetTestTypeValuesCommandCloud( - { projectKey: "CYP", xrayClient: xrayClient }, - logger, - new ConstantCommand(logger, ["CYP-123", "CYP-456", "CYP-789"]) - ); - xrayClient.getTestTypes.withArgs("CYP", "CYP-123", "CYP-456", "CYP-789").resolves({ - ["CYP-123"]: "Cucumber", - ["CYP-456"]: "Generic", - ["CYP-789"]: "Manual", - }); - const summaries = await command.compute(); - expect(summaries).to.deep.eq({ - ["CYP-123"]: "Cucumber", - ["CYP-456"]: "Generic", - ["CYP-789"]: "Manual", - }); - }); - - it("displays a warning for issues which do not exist", async () => { - const logger = getMockedLogger(); - const xrayClient = getMockedXrayClient("cloud"); - const command = new GetTestTypeValuesCommandCloud( - { projectKey: "CYP", xrayClient: xrayClient }, - logger, - new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) - ); - xrayClient.getTestTypes.withArgs("CYP", "CYP-123", "CYP-789", "CYP-456").resolves({ - ["CYP-123"]: "Cucumber", - }); - expect(await command.compute()).to.deep.eq({ - ["CYP-123"]: "Cucumber", - }); - expect(logger.message).to.have.been.calledWithExactly( - Level.WARNING, - dedent(` - Failed to retrieve test types of issues: - - CYP-456 - CYP-789 - `) - ); - }); - - it("throws when encountering failures", async () => { - const logger = getMockedLogger(); - const xrayClient = getMockedXrayClient("cloud"); - const command = new GetTestTypeValuesCommandCloud( - { projectKey: "CYP", xrayClient: xrayClient }, - logger, - new ConstantCommand(logger, ["CYP-123", "CYP-789", "CYP-456"]) - ); - xrayClient.getTestTypes - .withArgs("CYP", "CYP-123", "CYP-789", "CYP-456") - .rejects(new Error("Connection timeout")); - await expect(command.compute()).to.eventually.be.rejectedWith("Connection timeout"); - }); - }); -}); diff --git a/src/hooks/util/commands/jira/get-test-type-values-command.ts b/src/hooks/util/commands/jira/get-test-type-values-command.ts deleted file mode 100644 index 6f213f35..00000000 --- a/src/hooks/util/commands/jira/get-test-type-values-command.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { XrayClientCloud } from "../../../../client/xray/xray-client-cloud"; -import { Issue } from "../../../../types/jira/responses/issue"; -import { StringMap } from "../../../../types/util"; -import { dedent } from "../../../../util/dedent"; -import { extractNestedString } from "../../../../util/extraction"; -import { Level, Logger } from "../../../../util/logging"; -import { Command, Computable } from "../../../command"; -import { JiraField } from "./extract-field-id-command"; -import { GetFieldValuesCommand } from "./get-field-values-command"; - -export class GetTestTypeValuesCommandServer extends GetFieldValuesCommand { - protected async computeResult(): Promise> { - // Field property example: - // customfield_12100: { - // value: "Cucumber", - // id: "12702", - // disabled: false - // } - return await this.extractJiraFieldValues((issue: Issue, fieldId: string) => - extractNestedString(issue.fields, [fieldId, "value"]) - ); - } -} - -interface Parameters { - projectKey: string; - xrayClient: XrayClientCloud; -} - -export class GetTestTypeValuesCommandCloud extends Command, Parameters> { - private readonly issueKeys: Computable; - constructor(parameters: Parameters, logger: Logger, issueKeys: Computable) { - super(parameters, logger); - this.issueKeys = issueKeys; - } - - protected async computeResult(): Promise> { - const issueKeys = await this.issueKeys.compute(); - const testTypes = await this.parameters.xrayClient.getTestTypes( - this.parameters.projectKey, - ...issueKeys - ); - const missingTypes = issueKeys.filter((key) => !(key in testTypes)); - if (missingTypes.length > 0) { - missingTypes.sort(); - this.logger.message( - Level.WARNING, - dedent(` - Failed to retrieve test types of issues: - - ${missingTypes.join("\n")} - `) - ); - } - return testTypes; - } -} diff --git a/src/hooks/util/commands/jira/transition-issue-command.spec.ts b/src/hooks/util/commands/jira/transition-issue-command.spec.ts new file mode 100644 index 00000000..b8f45983 --- /dev/null +++ b/src/hooks/util/commands/jira/transition-issue-command.spec.ts @@ -0,0 +1,32 @@ +import chai, { expect } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import path from "path"; +import { getMockedJiraClient, getMockedLogger } from "../../../../../test/mocks"; +import { Level } from "../../../../util/logging"; +import { ConstantCommand } from "../constant-command"; +import { TransitionIssueCommand } from "./transition-issue-command"; + +chai.use(chaiAsPromised); + +describe(path.relative(process.cwd(), __filename), () => { + describe(TransitionIssueCommand.name, () => { + it("transitions issues", async () => { + const logger = getMockedLogger(); + const jiraClient = getMockedJiraClient(); + jiraClient.transitionIssue.onFirstCall().resolves(); + const command = new TransitionIssueCommand( + { jiraClient: jiraClient, transition: { id: "5" } }, + logger, + new ConstantCommand(logger, "CYP-123") + ); + await command.compute(); + expect(logger.message).to.have.been.calledWithExactly( + Level.INFO, + "Transitioning test execution issue CYP-123" + ); + expect(jiraClient.transitionIssue).to.have.been.calledOnceWithExactly("CYP-123", { + id: "5", + }); + }); + }); +}); diff --git a/src/hooks/util/commands/jira/transition-issue-command.ts b/src/hooks/util/commands/jira/transition-issue-command.ts new file mode 100644 index 00000000..ad9691aa --- /dev/null +++ b/src/hooks/util/commands/jira/transition-issue-command.ts @@ -0,0 +1,33 @@ +import { JiraClient } from "../../../../client/jira/jira-client"; +import { IssueUpdate } from "../../../../types/jira/responses/issue-update"; +import { Level, Logger } from "../../../../util/logging"; +import { Command, Computable } from "../../../command"; + +interface Parameters { + jiraClient: JiraClient; + transition: Exclude; +} + +export class TransitionIssueCommand extends Command { + private readonly resolvedExecutionIssueKey: Computable; + constructor( + parameters: Parameters, + logger: Logger, + resolvedExecutionIssueKey: Computable + ) { + super(parameters, logger); + this.resolvedExecutionIssueKey = resolvedExecutionIssueKey; + } + + protected async computeResult(): Promise { + const resolvedExecutionIssueKey = await this.resolvedExecutionIssueKey.compute(); + this.logger.message( + Level.INFO, + `Transitioning test execution issue ${resolvedExecutionIssueKey}` + ); + await this.parameters.jiraClient.transitionIssue( + resolvedExecutionIssueKey, + this.parameters.transition + ); + } +} diff --git a/src/plugin.spec.ts b/src/plugin.spec.ts index 68314662..d4ccab45 100644 --- a/src/plugin.spec.ts +++ b/src/plugin.spec.ts @@ -116,7 +116,6 @@ describe(path.relative(process.cwd(), __filename), () => { summary: "bonjour", testEnvironments: "field_123", testPlan: "there", - testType: "!", }, projectKey: "ABC", testExecutionIssue: { @@ -160,7 +159,6 @@ describe(path.relative(process.cwd(), __filename), () => { summary: "bonjour", testEnvironments: "field_123", testPlan: "there", - testType: "!", }, projectKey: "ABC", testExecutionIssue: { diff --git a/src/types/plugin.ts b/src/types/plugin.ts index e9cb6de7..67e5af7f 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -3,8 +3,8 @@ import { AxiosRequestConfig } from "axios"; import { AxiosRestClient, RequestsOptions } from "../client/https/requests"; import { JiraClient } from "../client/jira/jira-client"; import { XrayClient } from "../client/xray/xray-client"; +import { CypressRunResultType } from "./cypress/cypress"; import { IssueUpdate } from "./jira/responses/issue-update"; -import { MaybeFunction } from "./util"; /** * Models all options for configuring the behaviour of the plugin. @@ -95,20 +95,26 @@ export interface JiraFieldIds { */ testEnvironments?: string; /** - * The test plan field ID of Xray test (execution) issues. + * The Jira field ID of test plans in Xray test (execution) issues. * * *Note: This option is required for server instances only. Xray cloud provides ways to * retrieve test plan field information independently of Jira.* */ testPlan?: string; +} + +/** + * Wrapper type around Jira's issue update type with some additional properties. + */ +export type PluginIssueUpdate = IssueUpdate & { /** - * The test type field ID of Xray test issues. + * An execution issue key to attach run results to. If omitted, Xray will always create + * a new test execution issue with each upload. * - * *Note: This option is required for server instances only. Xray cloud provides ways to - * retrieve test type field information independently of Jira.* + * @example "CYP-123" */ - testType?: string; -} + key?: string; +}; /** * Jira-specific options that control how the plugin interacts with Jira. @@ -126,8 +132,8 @@ export interface JiraOptions { * `Description`, ...) just fine. Still, providing the fields' IDs here might become necessary * in the following scenarios: * - Your Jira language setting is a language other than English. For example, when the plugin - * tries to access the `Summary` field and the Jira language is set to French, access will fail - * because Jira only provides access to a field called `Résumé` instead. + * tries to access the `Test Plan` field and the Jira language is set to French, access will + * fail because Jira only provides access to a field called `Plan de Test` instead. * - Your Jira project contains several fields with identical names. * * *Note: In case you don't know these properties or if you are unsure whether they are really @@ -139,7 +145,6 @@ export interface JiraOptions { * @example * ```ts * fields: { - * description: "description", * testPlan: "customfield_12643" * } * ``` @@ -156,8 +161,6 @@ export interface JiraOptions { * create or modify with the run results. The value must match the format of Jira's issue * create/update payloads. * - * @example - * * ```ts * testExecutionIssue: { * key: "PRJ-16", @@ -172,24 +175,48 @@ export interface JiraOptions { * } * ``` * + * You can also return the issue data from a function in case you need dynamic values based on + * data computed during the test run. + * + * ```ts + * const executionIssueData = { + * fields: { + * issuetype: { + * name: "Test Execution", + * }, + * summary: "My default summary", + * description: "My default description", + * }, + * }; + * await configureXrayPlugin(on, config, { + * jira: { + * projectKey: "CYP", + * testExecution: ({ results }) => { + * if (results.totalFailed > 0) { + * executionIssueData.fields.summary = "Failed test execution"; + * } + * return executionIssueData; + * }, + * url: "https://example.org", + * }, + * }); + * ``` + * * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post * @see https://developer.atlassian.com/server/jira/platform/rest/v10000/api-group-issue/#api-api-2-issue-post */ - testExecutionIssue?: MaybeFunction< - IssueUpdate & { - /** - * An execution issue key to attach run results to. If omitted, Jira will always create a new - * test execution issue with each upload. - * - * @example "CYP-123" - */ - key?: string; - } - >; + testExecutionIssue?: + | ((args: { + /** + * The Cypress run results. + */ + results: CypressRunResultType; + }) => PluginIssueUpdate | Promise) + | PluginIssueUpdate; /** * The description of the test execution issue, which will be used both for new test execution * issues as well as for updating existing issues (if provided through - * {@link JiraOptions.testExecutionIssueKey}). + * {@link JiraOptions.testExecutionIssue}). * * If omitted, test execution issues will have the following description: * ```ts @@ -216,8 +243,6 @@ export interface JiraOptions { * An execution issue key to attach run results to. If omitted, Jira will always create a new * test execution issue with each upload. * - * *Note: it must be prefixed with the project key.* - * * @example "CYP-123" * * @deprecated Will be removed in version `8.0.0`. Please use the following instead: @@ -237,7 +262,7 @@ export interface JiraOptions { /** * The summary of the test execution issue, which will be used both for new test execution * issues as well as for updating existing issues (if provided through - * {@link JiraOptions.testExecutionIssueKey}). + * {@link JiraOptions.testExecutionIssue}). * * If omitted, test execution issues will be named as follows: * ```ts @@ -288,14 +313,21 @@ export interface JiraOptions { /** * A test plan issue key to attach the execution to. * - * *Note: it must be prefixed with the project key.* - * * @example "CYP-567" */ - testPlanIssueKey?: MaybeFunction; + testPlanIssueKey?: + | ((args: { + /** + * The Cypress run results. + */ + results: CypressRunResultType; + }) => Promise | string) + | string; /** * The issue type name of test plans. By default, Xray calls them `Test Plan`, but it's possible * that they have been renamed or translated in your Jira instance. + * + * @deprecated Unused, will be removed in version `8.0.0`. */ testPlanIssueType?: string; /** diff --git a/src/types/util.ts b/src/types/util.ts index c0bbb822..9248365c 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -35,9 +35,9 @@ export type StringMap = Record; * } * ``` */ -export type Remap = { +export type Remap = { [K in keyof Required]: Required[K] extends object // shortcuts simple types - ? K extends E // shortcuts excluded properties + ? K extends ArrayElementType // shortcuts excluded properties ? V : Required[K] extends unknown[] // shortcuts array types ? V @@ -50,4 +50,8 @@ export type Remap = (() => Promise | T) | T; +export type MaybeFunction

= ((...args: P) => Promise | T) | T; + +// See: https://stackoverflow.com/a/51399781 +type ArrayElementType = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; diff --git a/src/util/compare.spec.ts b/src/util/compare.spec.ts new file mode 100644 index 00000000..fb38f5c9 --- /dev/null +++ b/src/util/compare.spec.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; +import path from "node:path"; +import { contains } from "./compare"; + +describe(path.relative(process.cwd(), __filename), () => { + describe(contains.name, () => { + describe("primitive types", () => { + it("bigint", () => { + expect(contains(BigInt(200), BigInt(200))).to.be.true; + }); + it("bigint (negative)", () => { + expect(contains(BigInt(200), BigInt(500))).to.be.false; + }); + it("boolean", () => { + expect(contains(true, true)).to.be.true; + }); + it("boolean (negative)", () => { + expect(contains(true, false)).to.be.false; + }); + it("function", () => { + expect(contains(console.log, console.log)).to.be.true; + }); + it("function (negative)", () => { + expect(contains(console.log, console.info)).to.be.false; + }); + it("number", () => { + expect(contains(42, 42)).to.be.true; + }); + it("number (negative)", () => { + expect(contains(42, 1000)).to.be.false; + }); + it("string", () => { + expect(contains("hello", "hello")).to.be.true; + }); + it("string (negative)", () => { + expect(contains("hello", "bye")).to.be.false; + }); + it("symbol", () => { + expect(contains(Symbol.for("abc"), Symbol.for("abc"))).to.be.true; + }); + it("symbol (negative)", () => { + expect(contains(Symbol.for("abc"), Symbol.for("def"))).to.be.false; + }); + it("undefined", () => { + expect(contains(undefined, undefined)).to.be.true; + }); + it("undefined (negative)", () => { + expect(contains(undefined, 42)).to.be.false; + }); + }); + + describe("arrays", () => { + it("equal", () => { + expect(contains([1, 2, 3, "hello", false], [1, 2, 3, "hello", false])).to.be.true; + }); + it("partially equal", () => { + expect(contains([1, 2, 3, "hello", false], [false, "hello", 3])).to.be.true; + }); + it("not equal", () => { + expect(contains([1, 2, 3, "hello", false], [true, "bye", 17])).to.be.false; + }); + it("not equal and no array", () => { + expect(contains(null, [1, 2, 3])).to.be.false; + }); + }); + + describe("objects", () => { + it("equal", () => { + expect(contains({ a: "b", c: 5, d: false }, { a: "b", c: 5, d: false })).to.be.true; + }); + it("partially equal", () => { + expect(contains({ a: "b", c: 5, d: false }, { c: 5, d: false })).to.be.true; + }); + it("not equal", () => { + expect(contains({ a: "b", c: 5, d: false }, { [5]: "oh no", x: "y" })).to.be.false; + }); + }); + + describe("complex", () => { + it("partially equal", () => { + expect( + contains( + { + a: "b", + c: 5, + d: [ + { e: 42, f: 100, g: "hi", h: [32, 1052] }, + null, + { x: [17, { y: null, z: "bonjour" }] }, + ], + }, + { + c: 5, + d: [{ g: "hi", h: [1052] }, { x: [{ z: "bonjour" }] }], + } + ) + ).to.be.true; + }); + }); + }); +}); diff --git a/src/util/compare.ts b/src/util/compare.ts index 356b1e35..40aa7499 100644 --- a/src/util/compare.ts +++ b/src/util/compare.ts @@ -19,13 +19,11 @@ export function contains(actual: unknown, expected: unknown): boolean { typeof actual === "undefined" ) { return actual === expected; - } - if (Array.isArray(expected)) { + } else if (Array.isArray(expected)) { if (Array.isArray(actual)) { return containsArray(actual, expected); } - } - if (typeof actual === "object") { + } else if (typeof actual === "object") { if (typeof expected === "object") { if (actual === null || expected === null) { return actual === expected; @@ -38,11 +36,16 @@ export function contains(actual: unknown, expected: unknown): boolean { function containsArray(actual: unknown[], expected: unknown[]): boolean { for (const elementExpected of expected) { + let containsElement = false; for (const elementActual of actual) { - if (!contains(elementActual, elementExpected)) { - return false; + if (contains(elementActual, elementExpected)) { + containsElement = true; + break; } } + if (!containsElement) { + return false; + } } return true; } diff --git a/src/util/functions.ts b/src/util/functions.ts index 1a26346e..b9e572d9 100644 --- a/src/util/functions.ts +++ b/src/util/functions.ts @@ -7,14 +7,19 @@ import { MaybeFunction } from "../types/util"; * @param value - the value * @returns the value or the callback result */ -export async function getOrCall(value: MaybeFunction): Promise { +export async function getOrCall

( + value: MaybeFunction, + ...args: P +): Promise { // See https://github.com/microsoft/TypeScript/issues/37663#issuecomment-1081610403 if (isFunction(value)) { - return await value(); + return await value(...args); } return value; } -function isFunction(value: unknown): value is (...args: unknown[]) => unknown { +function isFunction

( + value: MaybeFunction +): value is (...args: P) => Promise | T { return typeof value === "function"; } diff --git a/src/util/graph/logging/graph-logger.ts b/src/util/graph/logging/graph-logger.ts index 1ab32eb4..96516e18 100644 --- a/src/util/graph/logging/graph-logger.ts +++ b/src/util/graph/logging/graph-logger.ts @@ -86,7 +86,10 @@ export class ChainingGraphLogger { } } else if (includePredecessors) { for (const predecessor of graph.getPredecessors(currentVertex)) { - if (!queue.find(([v]) => v === predecessor)) { + if ( + !queue.find(([v]) => v === predecessor) && + chain.every((entry) => entry.vertex !== predecessor) + ) { queue.enqueue([predecessor, indent, true]); } } diff --git a/src/util/help.ts b/src/util/help.ts index f2362add..d3178490 100644 --- a/src/util/help.ts +++ b/src/util/help.ts @@ -24,8 +24,6 @@ export const HELP = { issuetype: `${BASE_URL}/section/configuration/jira/#issuetype`, }, }, - testExecutionIssueType: `${BASE_URL}/section/configuration/jira/#testExecutionIssueType`, - testPlanIssueType: `${BASE_URL}/section/configuration/jira/#testPlanIssueType`, url: `${BASE_URL}/section/configuration/jira/#url`, }, plugin: { diff --git a/test/mocks.ts b/test/mocks.ts index 788db87d..48b808c0 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -82,6 +82,9 @@ export function getMockedJiraClient(): SinonStubbedInstance { search: function (request: SearchRequest) { throw mockCalledUnexpectedlyError("search", request); }, + transitionIssue: function (issueIdOrKey: string, issueUpdateData: IssueUpdate) { + throw mockCalledUnexpectedlyError("transitionIssue", issueIdOrKey, issueUpdateData); + }, }; return makeTransparent(stub(client)); }