jest-qase-reporter 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,6 +135,12 @@ A test run will be performed and available at:
135
135
  https://app.qase.io/run/QASE_PROJECT_CODE
136
136
  ```
137
137
 
138
+ ### Multi-Project Support
139
+
140
+ Qase Jest Reporter supports sending test results to multiple Qase projects simultaneously. You can specify different test case IDs for each project using `qase.projects(mapping, name)`.
141
+
142
+ For detailed information, configuration, and examples, see the [Multi-Project Support Guide](docs/MULTI_PROJECT.md).
143
+
138
144
  ## Configuration
139
145
 
140
146
  Reporter options (* - required):
package/changelog.md CHANGED
@@ -1,3 +1,15 @@
1
+ # jest-qase-reporter@2.2.0
2
+
3
+ ## What's new
4
+
5
+ - Added support for multi-project support.
6
+
7
+ # jest-qase-reporter@2.1.4
8
+
9
+ ## What's new
10
+
11
+ - Added support for expected results and data for steps.
12
+
1
13
  # jest-qase-reporter@2.1.3
2
14
 
3
15
  ## What's new
package/dist/jest.d.ts CHANGED
@@ -1,98 +1,17 @@
1
1
  import { StepFunction } from 'qase-javascript-commons';
2
+ /** Project code → test case IDs for multi-project (testops_multi) mode. */
3
+ export type ProjectMapping = Record<string, number[]>;
2
4
  export declare const qase: {
3
5
  (caseId: number | string | number[] | string[], name: string): string;
4
- /**
5
- * Set a title for the test case
6
- * @param {string} value
7
- * @example
8
- * test('test', () => {
9
- * qase.title("Title");
10
- * expect(true).toBe(true);
11
- * });
12
- */
6
+ projects(mapping: ProjectMapping, name: string): string;
13
7
  title(value: string): void;
14
- /**
15
- * Ignore the test case
16
- * @example
17
- * test('test', () => {
18
- * qase.ignore();
19
- * expect(true).toBe(true);
20
- * });
21
- */
22
8
  ignore(): void;
23
- /**
24
- * Add a comment to the test case
25
- * @param {string} value
26
- * @example
27
- * test('test', () => {
28
- * qase.comment("Comment");
29
- * expect(true).toBe(true);
30
- * });
31
- */
32
9
  comment(value: string): void;
33
- /**
34
- * Set a suite for the test case
35
- * @param {string} value
36
- * @example
37
- * test('test', () => {
38
- * qase.suite("Suite");
39
- * expect(true).toBe(true);
40
- * });
41
- */
42
10
  suite(value: string): void;
43
- /**
44
- * Set fields for the test case
45
- * @param {Record<string, string>} values
46
- * @example
47
- * test('test', () => {
48
- * qase.fields({field: "value"});
49
- * expect(true).toBe(true);
50
- * });
51
- */
52
11
  fields(values: Record<string, string>): void;
53
- /**
54
- * Set parameters for the test case
55
- * @param {Record<string, string>} values
56
- * @example
57
- * test('test', () => {
58
- * qase.parameters({param: "value"});
59
- * expect(true).toBe(true);
60
- * });
61
- */
62
12
  parameters(values: Record<string, string>): void;
63
- /**
64
- * Set group params for the test case
65
- * @param {Record<string, string>} values
66
- * @example
67
- * test('test', () => {
68
- * qase.groupParameters({param: "value"});
69
- * expect(true).toBe(true);
70
- * });
71
- */
72
13
  groupParameters(values: Record<string, string>): void;
73
- /**
74
- * Add a step to the test case
75
- * @param name
76
- * @param body
77
- * @example
78
- * test('test', () => {
79
- * qase.step("Step", () => {
80
- * expect(true).toBe(true);
81
- * });
82
- * expect(true).toBe(true);
83
- * });
84
- */
85
- step(name: string, body: StepFunction): Promise<void>;
86
- /**
87
- * Add an attachment to the test case
88
- * @param attach
89
- * @example
90
- * test('test', () => {
91
- * qase.attach({ name: 'attachment.txt', content: 'Hello, world!', type: 'text/plain' });
92
- * qase.attach({ paths: ['/path/to/file', '/path/to/another/file']});
93
- * expect(true).toBe(true);
94
- * });
95
- */
14
+ step(name: string, body: StepFunction, expectedResult?: string, data?: string): Promise<void>;
96
15
  attach(attach: {
97
16
  name?: string;
98
17
  type?: string;
package/dist/jest.js CHANGED
@@ -12,6 +12,15 @@ const qase = (caseId, name) => {
12
12
  return `${name} (Qase ID: ${caseIds.join(',')})`;
13
13
  };
14
14
  exports.qase = qase;
15
+ /**
16
+ * Build test name with multi-project markers (for testops_multi mode).
17
+ * @param mapping — e.g. { PROJ1: [1, 2], PROJ2: [3] }
18
+ * @param name — test title
19
+ * @example test(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'Login flow'), () => { ... });
20
+ */
21
+ exports.qase.projects = (mapping, name) => {
22
+ return (0, qase_javascript_commons_1.formatTitleWithProjectMapping)(name, mapping);
23
+ };
15
24
  /**
16
25
  * Set a title for the test case
17
26
  * @param {string} value
@@ -22,6 +31,8 @@ exports.qase = qase;
22
31
  * });
23
32
  */
24
33
  exports.qase.title = (value) => {
34
+ // @ts-expect-error - global.Qase is dynamically added at runtime
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
25
36
  global.Qase.title(value);
26
37
  };
27
38
  /**
@@ -33,6 +44,8 @@ exports.qase.title = (value) => {
33
44
  * });
34
45
  */
35
46
  exports.qase.ignore = () => {
47
+ // @ts-expect-error - global.Qase is dynamically added at runtime
48
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
36
49
  global.Qase.ignore();
37
50
  };
38
51
  /**
@@ -45,6 +58,8 @@ exports.qase.ignore = () => {
45
58
  * });
46
59
  */
47
60
  exports.qase.comment = (value) => {
61
+ // @ts-expect-error - global.Qase is dynamically added at runtime
62
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
48
63
  global.Qase.comment(value);
49
64
  };
50
65
  /**
@@ -57,6 +72,8 @@ exports.qase.comment = (value) => {
57
72
  * });
58
73
  */
59
74
  exports.qase.suite = (value) => {
75
+ // @ts-expect-error - global.Qase is dynamically added at runtime
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
60
77
  global.Qase.suite(value);
61
78
  };
62
79
  /**
@@ -69,6 +86,8 @@ exports.qase.suite = (value) => {
69
86
  * });
70
87
  */
71
88
  exports.qase.fields = (values) => {
89
+ // @ts-expect-error - global.Qase is dynamically added at runtime
90
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
72
91
  global.Qase.fields(values);
73
92
  };
74
93
  /**
@@ -81,6 +100,8 @@ exports.qase.fields = (values) => {
81
100
  * });
82
101
  */
83
102
  exports.qase.parameters = (values) => {
103
+ // @ts-expect-error - global.Qase is dynamically added at runtime
104
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
84
105
  global.Qase.parameters(values);
85
106
  };
86
107
  /**
@@ -93,12 +114,16 @@ exports.qase.parameters = (values) => {
93
114
  * });
94
115
  */
95
116
  exports.qase.groupParameters = (values) => {
117
+ // @ts-expect-error - global.Qase is dynamically added at runtime
118
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
96
119
  global.Qase.groupParams(values);
97
120
  };
98
121
  /**
99
122
  * Add a step to the test case
100
123
  * @param name
101
124
  * @param body
125
+ * @param expectedResult
126
+ * @param data
102
127
  * @example
103
128
  * test('test', () => {
104
129
  * qase.step("Step", () => {
@@ -106,11 +131,25 @@ exports.qase.groupParameters = (values) => {
106
131
  * });
107
132
  * expect(true).toBe(true);
108
133
  * });
134
+ * @example
135
+ * test('test', () => {
136
+ * qase.step("Step", () => {
137
+ * expect(true).toBe(true);
138
+ * }, "Expected result", "Input data");
139
+ * expect(true).toBe(true);
140
+ * });
109
141
  */
110
- exports.qase.step = async (name, body) => {
111
- const runningStep = new qase_javascript_commons_1.QaseStep(name);
142
+ exports.qase.step = async (name, body, expectedResult, data) => {
143
+ const stepName = expectedResult || data
144
+ ? `${name} QaseExpRes:${expectedResult ? `: ${expectedResult}` : ''} QaseData:${data ? `: ${data}` : ''}`
145
+ : name;
146
+ const runningStep = new qase_javascript_commons_1.QaseStep(stepName);
112
147
  // eslint-disable-next-line @typescript-eslint/require-await
113
- await runningStep.run(body, async (step) => global.Qase.step(step));
148
+ await runningStep.run(body, async (step) => {
149
+ // @ts-expect-error - global.Qase is dynamically added at runtime
150
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return
151
+ return global.Qase.step(step);
152
+ });
114
153
  };
115
154
  /**
116
155
  * Add an attachment to the test case
@@ -127,6 +166,8 @@ exports.qase.attach = (attach) => {
127
166
  for (const file of attach.paths) {
128
167
  const attachmentName = path_1.default.basename(file);
129
168
  const contentType = (0, qase_javascript_commons_1.getMimeTypes)(file);
169
+ // @ts-expect-error - global.Qase is dynamically added at runtime
170
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
130
171
  global.Qase.attachment({
131
172
  file_path: file,
132
173
  size: 0,
@@ -139,6 +180,8 @@ exports.qase.attach = (attach) => {
139
180
  return;
140
181
  }
141
182
  if (attach.content) {
183
+ // @ts-expect-error - global.Qase is dynamically added at runtime
184
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
142
185
  global.Qase.attachment({
143
186
  file_path: null,
144
187
  size: attach.content.length,
@@ -103,5 +103,12 @@ export declare class JestQaseReporter implements Reporter {
103
103
  * @private
104
104
  */
105
105
  private removeQaseIdsFromTitle;
106
+ /**
107
+ * Extract expected result and data from step title and return cleaned string
108
+ * @param {string} input
109
+ * @returns {{expectedResult: string | null, data: string | null, cleanedString: string}}
110
+ * @private
111
+ */
112
+ private extractAndCleanStep;
106
113
  onRunnerEnd(): Promise<void>;
107
114
  }
package/dist/reporter.js CHANGED
@@ -63,6 +63,7 @@ class JestQaseReporter {
63
63
  frameworkName: 'jest',
64
64
  reporterName: 'jest-qase-reporter',
65
65
  });
66
+ // @ts-expect-error - global.Qase is dynamically added at runtime
66
67
  global.Qase = new global_1.Qase(this);
67
68
  this.metadata = this.createEmptyMetadata();
68
69
  }
@@ -208,6 +209,17 @@ class JestQaseReporter {
208
209
  this.metadata.ignore = true;
209
210
  }
210
211
  addStep(step) {
212
+ // Parse expectedResult and data from step name if present
213
+ // Only process text steps (not gherkin steps)
214
+ // Check if step is a text step (either explicitly TEXT type, undefined, or string 'text')
215
+ const isTextStep = (qase_javascript_commons_1.StepType && step.step_type === qase_javascript_commons_1.StepType.TEXT) || step.step_type === undefined || step.step_type === 'text';
216
+ if (isTextStep && step.data && 'action' in step.data) {
217
+ const stepTextData = step.data;
218
+ const stepData = this.extractAndCleanStep(stepTextData.action);
219
+ stepTextData.action = stepData.cleanedString;
220
+ stepTextData.expected_result = stepData.expectedResult;
221
+ stepTextData.data = stepData.data;
222
+ }
211
223
  this.metadata.steps.push(step);
212
224
  }
213
225
  addAttachment(attachment) {
@@ -233,11 +245,13 @@ class JestQaseReporter {
233
245
  }).join('\n\n'));
234
246
  error.stack = value.failureMessages.join('\n\n');
235
247
  }
236
- const ids = JestQaseReporter.getCaseId(value.title);
248
+ const parsed = (0, qase_javascript_commons_1.parseProjectMappingFromTitle)(value.title);
237
249
  const filePath = this.getCurrentTestPath(path);
250
+ const hasProjectMapping = Object.keys(parsed.projectMapping).length > 0;
251
+ const ids = hasProjectMapping ? [] : parsed.legacyIds;
238
252
  // Determine status based on error type
239
- const testStatus = (0, qase_javascript_commons_1.determineTestStatus)(error || null, value.status);
240
- return {
253
+ const testStatus = (0, qase_javascript_commons_1.determineTestStatus)(error ?? null, value.status);
254
+ const result = {
241
255
  attachments: [],
242
256
  author: null,
243
257
  execution: {
@@ -257,10 +271,14 @@ class JestQaseReporter {
257
271
  run_id: null,
258
272
  signature: this.getSignature(filePath, value.fullName, ids, {}),
259
273
  steps: [],
260
- testops_id: ids.length > 0 ? ids : null,
274
+ testops_id: parsed.legacyIds.length > 0 && !hasProjectMapping
275
+ ? (parsed.legacyIds.length === 1 ? parsed.legacyIds[0] : parsed.legacyIds)
276
+ : null,
277
+ testops_project_mapping: hasProjectMapping ? parsed.projectMapping : null,
261
278
  id: (0, uuid_1.v4)(),
262
- title: this.removeQaseIdsFromTitle(value.title),
279
+ title: parsed.cleanedTitle || this.removeQaseIdsFromTitle(value.title),
263
280
  };
281
+ return result;
264
282
  }
265
283
  /**
266
284
  * @returns {Metadata}
@@ -291,6 +309,32 @@ class JestQaseReporter {
291
309
  }
292
310
  return title;
293
311
  }
312
+ /**
313
+ * Extract expected result and data from step title and return cleaned string
314
+ * @param {string} input
315
+ * @returns {{expectedResult: string | null, data: string | null, cleanedString: string}}
316
+ * @private
317
+ */
318
+ extractAndCleanStep(input) {
319
+ let expectedResult = null;
320
+ let data = null;
321
+ let cleanedString = input;
322
+ const hasExpectedResult = input.includes('QaseExpRes:');
323
+ const hasData = input.includes('QaseData:');
324
+ if (hasExpectedResult || hasData) {
325
+ const regex = /QaseExpRes:\s*:?\s*(.*?)\s*(?=QaseData:|$)QaseData:\s*:?\s*(.*)?/;
326
+ const match = input.match(regex);
327
+ if (match) {
328
+ expectedResult = match[1]?.trim() ?? null;
329
+ data = match[2]?.trim() ?? null;
330
+ cleanedString = input
331
+ .replace(/QaseExpRes:\s*:?\s*.*?(?=QaseData:|$)/, '')
332
+ .replace(/QaseData:\s*:?\s*.*/, '')
333
+ .trim();
334
+ }
335
+ }
336
+ return { expectedResult, data, cleanedString };
337
+ }
294
338
  async onRunnerEnd() {
295
339
  await this.reporter.publish();
296
340
  }
@@ -0,0 +1,53 @@
1
+ # Multi-Project Support in Jest
2
+
3
+ Qase Jest Reporter supports sending test results to multiple Qase projects simultaneously. This feature allows you to report the same test execution to different projects with different test case IDs, which is useful when:
4
+
5
+ * You need to report the same test to different projects
6
+ * Different projects track the same functionality with different test case IDs
7
+ * You want to maintain separate test runs for different environments or teams
8
+
9
+ ## Configuration
10
+
11
+ For detailed configuration options, refer to the [qase-javascript-commons README](../../qase-javascript-commons/README.md#multi-project-support).
12
+
13
+ ### Basic Multi-Project Configuration
14
+
15
+ Set `mode` to `testops_multi` in your Jest reporter options (e.g. in `jest.config.js` or `qase.config.json`) and add the `testops_multi` section with `default_project` and `projects`.
16
+
17
+ ## Using `qase.projects(mapping, name)`
18
+
19
+ Use `qase.projects(mapping, name)` to set the test title with multi-project markers. The first argument is the mapping (project code → array of case IDs); the second is the test name. Use the returned string as the test name:
20
+
21
+ ```javascript
22
+ const { qase } = require('jest-qase-reporter');
23
+
24
+ // Single project with single ID
25
+ test(qase(100, 'login flow'), () => { ... });
26
+
27
+ // Multi-project: one test, multiple projects
28
+ test(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'login flow'), () => { ... });
29
+
30
+ // Multiple IDs per project
31
+ test(qase.projects({ PROJ1: [10, 11], PROJ2: [20] }, 'checkout'), () => { ... });
32
+ ```
33
+
34
+ Project codes (e.g. `PROJ1`, `PROJ2`) must match `testops_multi.projects[].code` in your config.
35
+
36
+ ## Tests Without Project Mapping
37
+
38
+ Tests that do not use `qase.projects()` and have no `(Qase PROJ: ids)` in the title are sent to the `default_project`. If they use `qase(id, name)` (single-project), that ID is used for the default project.
39
+
40
+ ## Important Notes
41
+
42
+ 1. **Project codes must match**: Codes in `qase.projects({ PROJ1: [1], ... })` must match `testops_multi.projects[].code`.
43
+ 2. **Mode**: Set `mode` to `testops_multi` in reporter config.
44
+ 3. **Title format**: The helper produces a title like `Name (Qase PROJ1: 1,2) (Qase PROJ2: 3)` so the reporter can parse the mapping.
45
+
46
+ ## Examples
47
+
48
+ See the [multi-project Jest example](../../examples/multiProject/jest/) for a complete runnable setup.
49
+
50
+ ## Troubleshooting
51
+
52
+ * Verify `mode` is `testops_multi` and project codes in `qase.projects()` match the config.
53
+ * Ensure the first argument to `test()` is the string returned by `qase.projects(mapping, name)` (or an equivalent title with markers).
package/docs/usage.md CHANGED
@@ -65,19 +65,37 @@ The reporter uses the title from the `test.step` function as the step title. By
65
65
  Additionally, these steps get their own result in the Qase Test run, offering a well-organized summary of the test flow. This helps quickly identify the cause of any failures.
66
66
 
67
67
  ```javascript
68
+ const { qase } = require("jest-qase-reporter/jest");
69
+
68
70
  test('A Test case with steps, updated from code', async () => {
69
- await test.step('Initialize the environment', async () => {
71
+ await qase.step('Initialize the environment', async () => {
70
72
  // Set up test environment
71
73
  });
72
- await test.step('Test Core Functionality of the app', async () => {
74
+ await qase.step('Test Core Functionality of the app', async () => {
73
75
  // Exercise core functionality
74
76
  });
75
77
 
76
- await test.step('Verify Expected Behavior of the app', async () => {
78
+ await qase.step('Verify Expected Behavior of the app', async () => {
77
79
  // Assert expected behavior
78
80
  });
79
81
  });
80
82
  ```
83
+
84
+ #### Steps with Expected Result and Data
85
+
86
+ ```javascript
87
+ const { qase } = require("jest-qase-reporter/jest");
88
+
89
+ test('A Test case with steps including expected results and data', async () => {
90
+ await qase.step('Click button', async () => {
91
+ // Click action
92
+ }, 'Button should be clicked', 'Button data');
93
+
94
+ await qase.step('Fill form', async () => {
95
+ // Form filling action
96
+ }, 'Form should be filled', 'Form input data');
97
+ });
98
+ ```
81
99
  <br>
82
100
 
83
101
  ### Fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-qase-reporter",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Qase TMS Jest Reporter",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,18 +45,18 @@
45
45
  "dependencies": {
46
46
  "lodash.get": "^4.4.2",
47
47
  "lodash.has": "^4.5.2",
48
- "qase-javascript-commons": "~2.4.2",
49
- "uuid": "^9.0.0"
48
+ "qase-javascript-commons": "~2.5.0",
49
+ "uuid": "^9.0.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@jest/globals": "^29.5.0",
53
- "@jest/reporters": "^29.5.0",
54
- "@jest/test-result": "^29.5.0",
55
- "@types/jest": "^29.5.2",
56
- "@types/lodash.get": "^4.4.7",
57
- "@types/lodash.has": "^4.5.7",
58
- "jest": "^29.5.0",
59
- "ts-jest": "^29.1.0"
52
+ "@jest/globals": "^29.7.0",
53
+ "@jest/reporters": "^29.7.0",
54
+ "@jest/test-result": "^29.7.0",
55
+ "@types/jest": "^29.5.14",
56
+ "@types/lodash.get": "^4.4.9",
57
+ "@types/lodash.has": "^4.5.9",
58
+ "jest": "^29.7.0",
59
+ "ts-jest": "^29.4.5"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "jest": ">=28.0.0"
@@ -2,7 +2,9 @@
2
2
  "extends": "./tsconfig.json",
3
3
 
4
4
  "compilerOptions": {
5
- "noEmit": false
5
+ "noEmit": false,
6
+ "skipLibCheck": true,
7
+ "types": []
6
8
  },
7
9
 
8
10
  "include": ["./src/**/*.ts"]