cypress-qase-reporter 3.1.1 → 3.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
@@ -259,6 +259,12 @@ module.exports = cypress.defineConfig({
259
259
  Check out the example of configuration for multiple reporters in the
260
260
  [demo project](../examples/cypress/cypress.config.js).
261
261
 
262
+ ### Multi-Project Support
263
+
264
+ Qase Cypress Reporter supports sending test results to multiple Qase projects simultaneously. You can specify different test case IDs for each project using `qase.projects(mapping, it(...))`.
265
+
266
+ For detailed information, configuration, and examples, see the [Multi-Project Support Guide](docs/MULTI_PROJECT.md).
267
+
262
268
  ## Cucumber/Gherkin Integration
263
269
 
264
270
  If you use Cucumber with Gherkin feature files in your Cypress tests, Qase reporter provides full support for both the legacy `cypress-cucumber-preprocessor` and the modern `@badeball/cypress-cucumber-preprocessor`.
package/changelog.md CHANGED
@@ -1,3 +1,15 @@
1
+ # cypress-qase-reporter@3.2.0
2
+
3
+ ## What's new
4
+
5
+ - Added support for multi-project support.
6
+
7
+ # cypress-qase-reporter@3.1.2
8
+
9
+ ## What's new
10
+
11
+ - Improved handling of skipped tests due to failing beforeEach hooks.
12
+
1
13
  # cypress-qase-reporter@3.1.1
2
14
 
3
15
  ## What's new
package/dist/mocha.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Test } from 'mocha';
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[], test: Test): Test;
6
+ projects(mapping: ProjectMapping, nameOrTest: string | Test): string | Test;
4
7
  title(value: string): Cypress.Chainable<JQuery<void>>;
5
8
  fields(values: Record<string, string>): Cypress.Chainable<JQuery<void>>;
6
9
  ignore(): Cypress.Chainable<JQuery<void>>;
package/dist/mocha.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.qase = void 0;
4
+ const qase_javascript_commons_1 = require("qase-javascript-commons");
4
5
  const qase = (caseId, test) => {
5
6
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6
7
  if (!test?.title) {
@@ -11,6 +12,24 @@ const qase = (caseId, test) => {
11
12
  return test;
12
13
  };
13
14
  exports.qase = qase;
15
+ /**
16
+ * Build test title with multi-project markers (for testops_multi mode).
17
+ * Supports two usages:
18
+ * - it(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'Login flow'), () => { ... }) — returns formatted title string.
19
+ * - qase.projects({ PROJ1: [100], PROJ2: [200] }, it('Login flow', () => { ... })) — mutates test.title and returns the test.
20
+ * @param mapping — e.g. { PROJ1: [1, 2], PROJ2: [3] }
21
+ * @param nameOrTest — test title string or Mocha Test (when passing it(...)); the test title is updated with markers so the reporter can parse testops_project_mapping.
22
+ */
23
+ function isTestObject(nameOrTest) {
24
+ return Boolean(nameOrTest && typeof nameOrTest === 'object' && 'title' in nameOrTest && typeof nameOrTest.title === 'string');
25
+ }
26
+ exports.qase.projects = (mapping, nameOrTest) => {
27
+ if (isTestObject(nameOrTest)) {
28
+ nameOrTest.title = (0, qase_javascript_commons_1.formatTitleWithProjectMapping)(nameOrTest.title, mapping);
29
+ return nameOrTest;
30
+ }
31
+ return (0, qase_javascript_commons_1.formatTitleWithProjectMapping)(String(nameOrTest), mapping);
32
+ };
14
33
  /**
15
34
  * Set a title for the test case
16
35
  * @param {string} value
@@ -13,17 +13,12 @@ export declare class CypressQaseReporter extends reporters.Base {
13
13
  /**
14
14
  * @type {RegExp}
15
15
  */
16
+ /** @deprecated Use parseProjectMappingFromTitle from qase-javascript-commons for multi-project support. */
16
17
  static qaseIdRegExp: RegExp;
17
18
  /**
18
19
  * @type {Record<CypressState, TestStatusEnum>}
19
20
  */
20
21
  static statusMap: Record<CypressState, TestStatusEnum>;
21
- /**
22
- * @param {string} title
23
- * @returns {number[]}
24
- * @private
25
- */
26
- private static getCaseId;
27
22
  /**
28
23
  * @type {string | undefined}
29
24
  * @private
@@ -77,6 +72,13 @@ export declare class CypressQaseReporter extends reporters.Base {
77
72
  * @param {Suite} suite
78
73
  * @private
79
74
  */
75
+ /**
76
+ * Recursively collect all tests from a suite and its nested suites
77
+ * @param {Suite} suite
78
+ * @param {Test[]} tests
79
+ * @private
80
+ */
81
+ private collectAllTestsFromSuite;
80
82
  private handleSkippedTestsInSuite;
81
83
  /**
82
84
  * Add a test result for a skipped test (due to beforeEach failure)
@@ -107,12 +109,6 @@ export declare class CypressQaseReporter extends reporters.Base {
107
109
  * @private
108
110
  */
109
111
  private removeQaseIdsFromTitle;
110
- /**
111
- * Extracts numbers from @qaseid tags, regardless of case.
112
- * @param tags - An array of tags to process.
113
- * @returns An array of numbers extracted from the tags.
114
- */
115
- private extractQaseIds;
116
112
  private convertCypressMessages;
117
113
  private getSteps;
118
114
  }
package/dist/reporter.js CHANGED
@@ -23,6 +23,7 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
23
23
  /**
24
24
  * @type {RegExp}
25
25
  */
26
+ /** @deprecated Use parseProjectMappingFromTitle from qase-javascript-commons for multi-project support. */
26
27
  static qaseIdRegExp = /\(Qase ID:? ([\d,]+)\)/;
27
28
  /**
28
29
  * @type {Record<CypressState, TestStatusEnum>}
@@ -32,15 +33,6 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
32
33
  passed: qase_javascript_commons_1.TestStatusEnum.passed,
33
34
  pending: qase_javascript_commons_1.TestStatusEnum.skipped,
34
35
  };
35
- /**
36
- * @param {string} title
37
- * @returns {number[]}
38
- * @private
39
- */
40
- static getCaseId(title) {
41
- const [, ids] = title.match(CypressQaseReporter.qaseIdRegExp) ?? [];
42
- return ids ? ids.split(',').map((id) => Number(id)) : [];
43
- }
44
36
  /**
45
37
  * @type {string | undefined}
46
38
  * @private
@@ -158,11 +150,28 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
158
150
  * @param {Suite} suite
159
151
  * @private
160
152
  */
153
+ /**
154
+ * Recursively collect all tests from a suite and its nested suites
155
+ * @param {Suite} suite
156
+ * @param {Test[]} tests
157
+ * @private
158
+ */
159
+ collectAllTestsFromSuite(suite, tests) {
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ const suiteTests = suite.tests ?? [];
162
+ tests.push(...suiteTests);
163
+ // Recursively process nested suites
164
+ const nestedSuites = suite.suites ?? [];
165
+ for (const nestedSuite of nestedSuites) {
166
+ this.collectAllTestsFromSuite(nestedSuite, tests);
167
+ }
168
+ }
161
169
  handleSkippedTestsInSuite(suite) {
162
- // Get tests only from the current suite (not nested suites, they will be processed separately)
163
- const tests = suite.tests ?? [];
170
+ // Collect all tests from this suite and nested suites recursively
171
+ const allTests = [];
172
+ this.collectAllTestsFromSuite(suite, allTests);
164
173
  // Find tests that were not processed (skipped due to beforeEach failure)
165
- for (const test of tests) {
174
+ for (const test of allTests) {
166
175
  // Skip if test was already processed (e.g., first test that got EVENT_TEST_FAIL)
167
176
  if (!this.isTestProcessed(test)) {
168
177
  // Test was skipped due to beforeEach failure, report it as skipped
@@ -179,7 +188,9 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
179
188
  const end_time = Date.now();
180
189
  const duration = 0; // Skipped tests have no duration
181
190
  const start_time = this.testBeginTime || Date.now();
182
- const ids = CypressQaseReporter.getCaseId(test.title);
191
+ const fromTitle = (0, qase_javascript_commons_1.parseProjectMappingFromTitle)(test.title);
192
+ const legacyIds = [...fromTitle.legacyIds];
193
+ const projectMapping = { ...fromTitle.projectMapping };
183
194
  const testFileName = this.getTestFileName(test);
184
195
  const files = this.screenshotsFolder ?
185
196
  fileSearcher_1.FileSearcher.findFilesBeforeTime(this.screenshotsFolder, testFileName, new Date(start_time))
@@ -213,9 +224,14 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
213
224
  const file = this.getFile(test.parent);
214
225
  if (file) {
215
226
  const tags = (0, tagParser_1.extractTags)(file, test.title);
216
- ids.push(...this.extractQaseIds(tags));
227
+ const fromTags = (0, qase_javascript_commons_1.parseProjectMappingFromTags)(tags);
228
+ legacyIds.push(...fromTags.legacyIds);
229
+ for (const [code, idsFromTag] of Object.entries(fromTags.projectMapping)) {
230
+ projectMapping[code] = [...(projectMapping[code] ?? []), ...idsFromTag];
231
+ }
217
232
  }
218
233
  }
234
+ const hasProjectMapping = Object.keys(projectMapping).length > 0;
219
235
  const result = {
220
236
  attachments: attachments,
221
237
  author: null,
@@ -226,7 +242,7 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
226
242
  group_params: {},
227
243
  relations: relations,
228
244
  run_id: null,
229
- signature: this.getSignature(test, ids, {}),
245
+ signature: this.getSignature(test, hasProjectMapping ? [] : legacyIds, {}),
230
246
  steps: [],
231
247
  id: (0, uuid_1.v4)(),
232
248
  execution: {
@@ -237,8 +253,11 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
237
253
  stacktrace: null,
238
254
  thread: null,
239
255
  },
240
- testops_id: ids.length > 0 ? ids : null,
241
- title: this.removeQaseIdsFromTitle(test.title),
256
+ testops_id: !hasProjectMapping && legacyIds.length > 0
257
+ ? (legacyIds.length === 1 ? legacyIds[0] : legacyIds)
258
+ : null,
259
+ testops_project_mapping: hasProjectMapping ? projectMapping : null,
260
+ title: fromTitle.cleanedTitle || this.removeQaseIdsFromTitle(test.title),
242
261
  preparedAttachments: [],
243
262
  };
244
263
  void this.reporter.addTestResult(result);
@@ -259,7 +278,9 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
259
278
  manager_1.MetadataManager.clear();
260
279
  return;
261
280
  }
262
- const ids = CypressQaseReporter.getCaseId(test.title);
281
+ const fromTitle = (0, qase_javascript_commons_1.parseProjectMappingFromTitle)(test.title);
282
+ const legacyIds = [...fromTitle.legacyIds];
283
+ const projectMapping = { ...fromTitle.projectMapping };
263
284
  const testFileName = this.getTestFileName(test);
264
285
  const files = this.screenshotsFolder ?
265
286
  fileSearcher_1.FileSearcher.findFilesBeforeTime(this.screenshotsFolder, testFileName, new Date(this.testBeginTime))
@@ -323,10 +344,15 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
323
344
  const file = this.getFile(test.parent);
324
345
  if (file) {
325
346
  const tags = (0, tagParser_1.extractTags)(file, test.title);
326
- ids.push(...this.extractQaseIds(tags));
347
+ const fromTags = (0, qase_javascript_commons_1.parseProjectMappingFromTags)(tags);
348
+ legacyIds.push(...fromTags.legacyIds);
349
+ for (const [code, idsFromTag] of Object.entries(fromTags.projectMapping)) {
350
+ projectMapping[code] = [...(projectMapping[code] ?? []), ...idsFromTag];
351
+ }
327
352
  }
328
353
  }
329
354
  }
355
+ const hasProjectMapping = Object.keys(projectMapping).length > 0;
330
356
  const result = {
331
357
  attachments: attachments,
332
358
  author: null,
@@ -337,7 +363,7 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
337
363
  group_params: metadata?.groupParams ?? {},
338
364
  relations: relations,
339
365
  run_id: null,
340
- signature: this.getSignature(test, ids, metadata?.parameters ?? {}),
366
+ signature: this.getSignature(test, hasProjectMapping ? [] : legacyIds, metadata?.parameters ?? {}),
341
367
  steps: steps,
342
368
  id: (0, uuid_1.v4)(),
343
369
  execution: {
@@ -348,8 +374,11 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
348
374
  stacktrace: test.err?.stack ?? null,
349
375
  thread: null,
350
376
  },
351
- testops_id: ids.length > 0 ? ids : null,
352
- title: metadata?.title ?? this.removeQaseIdsFromTitle(test.title),
377
+ testops_id: !hasProjectMapping && legacyIds.length > 0
378
+ ? (legacyIds.length === 1 ? legacyIds[0] : legacyIds)
379
+ : null,
380
+ testops_project_mapping: hasProjectMapping ? projectMapping : null,
381
+ title: metadata?.title ?? (fromTitle.cleanedTitle || this.removeQaseIdsFromTitle(test.title)),
353
382
  preparedAttachments: [],
354
383
  };
355
384
  void this.reporter.addTestResult(result);
@@ -411,25 +440,6 @@ class CypressQaseReporter extends mocha_1.reporters.Base {
411
440
  }
412
441
  return title;
413
442
  }
414
- /**
415
- * Extracts numbers from @qaseid tags, regardless of case.
416
- * @param tags - An array of tags to process.
417
- * @returns An array of numbers extracted from the tags.
418
- */
419
- extractQaseIds(tags) {
420
- const qaseIdRegex = /@qaseid\((\d+(?:,\d+)*)\)/i;
421
- const qaseIds = [];
422
- for (const tag of tags) {
423
- const match = qaseIdRegex.exec(tag);
424
- if (match) {
425
- const ids = match[1]?.split(',').map(id => parseInt(id, 10));
426
- if (ids) {
427
- qaseIds.push(...ids);
428
- }
429
- }
430
- }
431
- return qaseIds;
432
- }
433
443
  convertCypressMessages(messages, testStatus) {
434
444
  const result = [];
435
445
  const lastIndex = messages.length - 1;
@@ -0,0 +1,107 @@
1
+ # Multi-Project Support in Cypress
2
+
3
+ Qase Cypress 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
+ To enable multi-project support, set the mode to `testops_multi` in your Cypress reporter options (e.g. in `cypress.config.js` or `qase.config.json`):
16
+
17
+ ```json
18
+ {
19
+ "mode": "testops_multi",
20
+ "testops": {
21
+ "api": { "token": "<token>", "host": "qase.io" },
22
+ "batch": { "size": 100 }
23
+ },
24
+ "testops_multi": {
25
+ "default_project": "PROJ1",
26
+ "projects": [
27
+ {
28
+ "code": "PROJ1",
29
+ "run": { "title": "PROJ1 Cypress Run", "complete": true }
30
+ },
31
+ {
32
+ "code": "PROJ2",
33
+ "run": { "title": "PROJ2 Cypress Run", "complete": true }
34
+ }
35
+ ]
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Using `qase.projects()`
41
+
42
+ The `qase.projects(mapping, nameOrTest)` helper lets you map a test to one or more projects and case IDs.
43
+
44
+ ### Pass the test (recommended)
45
+
46
+ Pass the result of `it()` as the second argument so the test title is updated with markers; the reporter will parse them and set `testops_project_mapping`:
47
+
48
+ ```javascript
49
+ const { qase } = require('cypress-qase-reporter');
50
+
51
+ describe('Suite', () => {
52
+ qase.projects({ PROJ1: [1], PROJ2: [2] }, it('A test reported to two projects', () => {
53
+ cy.visit('https://example.cypress.io');
54
+ cy.contains('type').click();
55
+ }));
56
+
57
+ qase.projects(
58
+ { PROJ1: [10, 11], PROJ2: [20] },
59
+ it('Multiple cases per project', () => {
60
+ cy.visit('https://example.cypress.io');
61
+ }),
62
+ );
63
+ });
64
+ ```
65
+
66
+ ### Pass the title string
67
+
68
+ You can also pass a title string; the helper returns the formatted title for use as the first argument to `it()`:
69
+
70
+ ```javascript
71
+ it(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'Login flow'), () => {
72
+ cy.visit('/login');
73
+ });
74
+ ```
75
+
76
+ ### Combining with other Qase methods
77
+
78
+ Use `qase.projects()` together with other Qase methods (e.g. `qase.title()`, `qase.attach()`) inside the test.
79
+
80
+ ## Tests Without Project Mapping
81
+
82
+ If a test does not use `qase.projects()` and has no `(Qase PROJ: ids)` markers in the title, it is sent to the `default_project` from your configuration. If the test also has no `(Qase ID: …)` legacy ID, the result is sent to the default project without linking to a test case.
83
+
84
+ ## Important Notes
85
+
86
+ 1. **Project codes must match**: The project codes in `qase.projects({ PROJ1: [1], ... })` must exactly match the codes in `testops_multi.projects[].code`.
87
+
88
+ 2. **Mode requirement**: Set `mode` to `testops_multi` in your reporter config. Single-project mode (`testops`) does not use project mapping.
89
+
90
+ 3. **Cucumber/BDD**: When using Cypress with Cucumber (e.g. `@badeball/cypress-cucumber-preprocessor`), use tags in feature files: `@qaseid.PROJ1(1) @qaseid.PROJ2(2)`. See [Cucumber documentation](cucumber.md).
91
+
92
+ ## Examples
93
+
94
+ See the [multi-project Cypress example](../../examples/multiProject/cypress/) for a complete runnable setup.
95
+
96
+ ## Troubleshooting
97
+
98
+ ### Results not appearing in projects
99
+
100
+ * Ensure `mode` is `testops_multi` in reporter options.
101
+ * Verify project codes in `qase.projects()` match `testops_multi.projects[].code`.
102
+ * For Cypress, ensure you pass the test to `qase.projects(mapping, it(...))` so the title is updated with markers, or use a title that already contains `(Qase PROJ: ids)`.
103
+
104
+ ### Results sent to wrong project
105
+
106
+ * Check the `default_project` setting for tests without explicit mapping.
107
+ * Project codes are case-sensitive and must match exactly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cypress-qase-reporter",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Qase Cypress Reporter",
5
5
  "homepage": "https://github.com/qase-tms/qase-javascript",
6
6
  "sideEffects": false,
@@ -51,7 +51,7 @@
51
51
  "author": "Qase Team <support@qase.io>",
52
52
  "license": "Apache-2.0",
53
53
  "dependencies": {
54
- "qase-javascript-commons": "~2.4.13",
54
+ "qase-javascript-commons": "~2.5.0",
55
55
  "uuid": "^9.0.1"
56
56
  },
57
57
  "peerDependencies": {