Skip to content

Commit

Permalink
Add dynamic test execution data configuration (#374)
Browse files Browse the repository at this point in the history
* Add tests

* Add integration test

* Disable debug

* Fix test

* Fix test
  • Loading branch information
csvtuda authored Aug 18, 2024
1 parent 4b5a897 commit 462ad72
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 39 deletions.
14 changes: 0 additions & 14 deletions src/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,20 +1164,6 @@ describe(path.relative(process.cwd(), __filename), () => {
"Plugin misconfiguration: test execution issue key ABC-123 does not belong to project CYP"
);
});
it("detects mismatched test plan issue keys", () => {
expect(() =>
initJiraOptions(
{},
{
projectKey: "CYP",
testPlanIssueKey: "ABC-456",
url: "https://example.org",
}
)
).to.throw(
"Plugin misconfiguration: test plan issue key ABC-456 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(
Expand Down
5 changes: 0 additions & 5 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,6 @@ export function initJiraOptions(
}
const testPlanIssueKey =
parse(env, ENV_NAMES.jira.testPlanIssueKey, asString) ?? options.testPlanIssueKey;
if (testPlanIssueKey && !testPlanIssueKey.startsWith(projectKey)) {
throw new Error(
`Plugin misconfiguration: test plan issue key ${testPlanIssueKey} does not belong to project ${projectKey}`
);
}
return {
attachVideos:
parse(env, ENV_NAMES.jira.attachVideos, asBoolean) ?? options.attachVideos ?? false,
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/after/after-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { ExecutableGraph } from "../../util/graph/executable-graph";
import { Level, Logger } from "../../util/logging";
Expand Down Expand Up @@ -402,7 +403,7 @@ function getConvertMultipartInfoCommand(
if (convertCommand) {
return convertCommand;
}
let textExecutionIssueDataCommand: Command<IssueUpdate> | undefined;
let textExecutionIssueDataCommand: Command<MaybeFunction<IssueUpdate>> | undefined;
if (options.jira.testExecutionIssue) {
textExecutionIssueDataCommand = getOrCreateConstantCommand(
graph,
Expand Down
16 changes: 9 additions & 7 deletions src/hooks/after/commands/conversion/convert-info-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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 { MultipartInfo } from "../../../../types/xray/requests/import-execution-multipart-info";
import { getOrCall } from "../../../../util/functions";
import { Logger } from "../../../../util/logging";
import { Command, Computable } from "../../../command";
import {
Expand All @@ -24,7 +26,7 @@ export abstract class ConvertInfoCommand extends Command<MultipartInfo, Paramete
private readonly testExecutionIssueType: Computable<IssueTypeDetails>;
private readonly runInformation: Computable<RunData>;
private readonly info?: {
custom?: Computable<IssueUpdate>;
custom?: Computable<MaybeFunction<IssueUpdate>>;
fieldIds?: {
testEnvironmentsId?: Computable<string>;
testPlanId?: Computable<string>;
Expand All @@ -38,7 +40,7 @@ export abstract class ConvertInfoCommand extends Command<MultipartInfo, Paramete
testExecutionIssueType: Computable<IssueTypeDetails>,
runInformation: Computable<RunData>,
info?: {
custom?: Computable<IssueUpdate>;
custom?: Computable<MaybeFunction<IssueUpdate>>;
fieldIds?: {
testEnvironmentsId?: Computable<string>;
testPlanId?: Computable<string>;
Expand All @@ -58,7 +60,7 @@ export abstract class ConvertInfoCommand extends Command<MultipartInfo, Paramete
const custom = await this.info?.custom?.compute();
const summary = await this.info?.summary?.compute();
const testExecutionIssueData: TestExecutionIssueDataServer = {
custom: custom,
custom: await getOrCall(custom),
description: this.parameters.jira.testExecutionIssueDescription,
issuetype: testExecutionIssueType,
projectKey: this.parameters.jira.projectKey,
Expand Down Expand Up @@ -104,7 +106,7 @@ export class ConvertInfoServerCommand extends ConvertInfoCommand {
const testPlandId = await this.testPlanId.compute();
testExecutionIssueData.testPlan = {
fieldId: testPlandId,
value: this.parameters.jira.testPlanIssueKey,
value: await getOrCall(this.parameters.jira.testPlanIssueKey),
};
}
if (this.parameters.xray.testEnvironments && this.testEnvironmentsId) {
Expand All @@ -119,13 +121,13 @@ export class ConvertInfoServerCommand extends ConvertInfoCommand {
}

export class ConvertInfoCloudCommand extends ConvertInfoCommand {
protected buildInfo(
protected async buildInfo(
runInformation: RunData,
testExecutionIssueData: TestExecutionIssueData
): MultipartInfo {
): Promise<MultipartInfo> {
if (this.parameters.jira.testPlanIssueKey) {
testExecutionIssueData.testPlan = {
value: this.parameters.jira.testPlanIssueKey,
value: await getOrCall(this.parameters.jira.testPlanIssueKey),
};
}
if (this.parameters.xray.testEnvironments) {
Expand Down
25 changes: 13 additions & 12 deletions src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AxiosRestClient, RequestsOptions } from "../client/https/requests";
import { JiraClient } from "../client/jira/jira-client";
import { XrayClient } from "../client/xray/xray-client";
import { IssueUpdate } from "./jira/responses/issue-update";
import { MaybeFunction } from "./util";

/**
* Models all options for configuring the behaviour of the plugin.
Expand Down Expand Up @@ -174,17 +175,17 @@ export interface JiraOptions {
* @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?: IssueUpdate & {
/**
* 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"
*/
key?: string;
};
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;
}
>;
/**
* 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
Expand Down Expand Up @@ -291,7 +292,7 @@ export interface JiraOptions {
*
* @example "CYP-567"
*/
testPlanIssueKey?: string;
testPlanIssueKey?: MaybeFunction<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.
Expand Down
5 changes: 5 additions & 0 deletions src/types/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ export type Remap<T extends object, V, E extends number | string | symbol = neve
: Remap<Required<T>[K], V, E>
: V;
};

/**
* Represents a value that may be wrapped in a callback.
*/
export type MaybeFunction<T> = (() => Promise<T> | T) | T;
25 changes: 25 additions & 0 deletions src/util/functions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from "chai";
import path from "node:path";
import { getOrCall } from "./functions";

describe(path.relative(process.cwd(), __filename), () => {
describe(getOrCall.name, () => {
it("returns unwrapped values", async () => {
expect(await getOrCall("hello")).to.eq("hello");
});

it("resolves sync callbacks", async () => {
expect(await getOrCall(() => 5)).to.eq(5);
});

it("resolves async callbacks", async () => {
expect(
await getOrCall(async () => {
return new Promise((resolve) => {
resolve(5);
});
})
).to.eq(5);
});
});
});
20 changes: 20 additions & 0 deletions src/util/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MaybeFunction } from "../types/util";

/**
* If the value is a function, evaluates it and returns the result. Otherwise, the value will be
* returned immediately.
*
* @param value - the value
* @returns the value or the callback result
*/
export async function getOrCall<T>(value: MaybeFunction<T>): Promise<T> {
// See https://github.com/microsoft/TypeScript/issues/37663#issuecomment-1081610403
if (isFunction(value)) {
return await value();
}
return value;
}

function isFunction<T extends (...args: unknown[]) => unknown>(value: unknown): value is T {
return typeof value === "function";
}
162 changes: 162 additions & 0 deletions test/integration/359.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { expect } from "chai";
import path from "path";
import process from "process";
import { dedent } from "../../src/util/dedent";
import { LOCAL_SERVER } from "../server-config";
import { runCypress, setupCypressProject } from "../sh";
import { getIntegrationClient } from "./clients";
import { getCreatedTestExecutionIssueKey } from "./util";

// ============================================================================================== //
// https://github.com/Qytera-Gmbh/cypress-xray-plugin/issues/359
// ============================================================================================== //

describe.only(path.relative(process.cwd(), __filename), () => {
for (const test of [
{
expectedLabels: [],
expectedSummary: "Integration test 359 (hardcoded)",
manualTest: "CYP-1139",
projectKey: "CYP",
service: "cloud",
testExecutionIssueData: dedent(`
{
fields: {
summary: "Integration test 359 (hardcoded)",
labels: LABELS
}
}
`),
title: "test execution issue data is hardcoded (cloud)",
xrayPassedStatus: "PASSED",
},
{
expectedLabels: ["x", "y"],
expectedSummary: "Integration test 359 (wrapped)",
manualTest: "CYP-1139",
projectKey: "CYP",
service: "cloud",
testExecutionIssueData: dedent(`
() => {
return {
fields: {
summary: "Integration test 359 (wrapped)",
labels: LABELS
}
};
}
`),
title: "test execution issue data is wrapped (cloud)",
xrayPassedStatus: "PASSED",
},
{
expectedLabels: [],
expectedSummary: "Integration test 359 (hardcoded)",
manualTest: "CYPLUG-461",
projectKey: "CYPLUG",
service: "server",
testExecutionIssueData: dedent(`
{
fields: {
summary: "Integration test 359 (hardcoded)",
labels: LABELS
}
}
`),
title: "test execution issue data is hardcoded (server)",
xrayPassedStatus: "PASS",
},
{
expectedLabels: ["x", "y"],
expectedSummary: "Integration test 359 (wrapped)",
manualTest: "CYPLUG-461",
projectKey: "CYPLUG",
service: "server",
testExecutionIssueData: dedent(`
() => {
return {
fields: {
summary: "Integration test 359 (wrapped)",
labels: LABELS
}
};
}
`),
title: "test execution issue data is wrapped (server)",
xrayPassedStatus: "PASS",
},
] as const) {
it(test.title, async () => {
const project = setupCypressProject({
configFileContent: dedent(`
const { defineConfig } = require("cypress");
const fix = require("cypress-on-fix");
const { configureXrayPlugin } = require("cypress-xray-plugin");
const LABELS = [];
module.exports = defineConfig({
video: false,
chromeWebSecurity: false,
e2e: {
specPattern: "**/*.cy.js",
async setupNodeEvents(on, config) {
const fixedOn = fix(on);
await configureXrayPlugin(fixedOn, config, {
jira: {
projectKey: "CYP",
testExecutionIssue: ${test.testExecutionIssueData}
},
xray: {
uploadResults: true,
testEnvironments: ["DEV"],
status: {
passed: "${test.xrayPassedStatus}"
}
},
plugin: {
debug: false,
},
});
fixedOn("task", {
"update-labels": (values) => LABELS.push(...values)
});
return config;
},
},
});
`),
testFiles: [
{
content: dedent(`
describe("${test.manualTest} template spec", () => {
it("passes", () => {
cy.visit("${LOCAL_SERVER.url}");
cy.task("update-labels", ${JSON.stringify(test.expectedLabels)})
});
});
`),
fileName: "spec.cy.js",
},
],
});

const output = runCypress(project.projectDirectory, {
includeDefaultEnv: test.service,
});

const testExecutionIssueKey = getCreatedTestExecutionIssueKey(
test.projectKey,
output,
"cypress"
);

const searchResult = await getIntegrationClient("jira", test.service).search({
fields: ["labels", "summary"],
jql: `issue in (${testExecutionIssueKey})`,
});
expect(searchResult[0].fields?.labels).to.deep.eq(test.expectedLabels);
expect(searchResult[0].fields?.summary).to.deep.eq(test.expectedSummary);
});
}
});
11 changes: 11 additions & 0 deletions test/sh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ export function runCypress(
...mergedEnv,
...options?.env,
};
fs.writeFileSync(
path.join(cwd, "cypress.env.json"),
JSON.stringify(
mergedEnv,
(...args) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return args[1] ? args[1] : undefined;
},
2
).replaceAll("CYPRESS_", "")
);
const result = childProcess.spawnSync(CYPRESS_EXECUTABLE, ["run"], {
cwd: cwd,
env: mergedEnv,
Expand Down

0 comments on commit 462ad72

Please sign in to comment.