qase-javascript-commons 2.4.17 → 2.5.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 +262 -2
- package/changelog.md +17 -0
- package/dist/config/config-validation-schema.d.ts +82 -0
- package/dist/config/config-validation-schema.js +65 -2
- package/dist/env/env-validation-schema.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/config/TestOpsOptionsType.d.ts +18 -0
- package/dist/models/index.d.ts +2 -2
- package/dist/models/test-result.d.ts +27 -0
- package/dist/models/test-result.js +34 -0
- package/dist/options/mode-enum.d.ts +2 -0
- package/dist/options/mode-enum.js +2 -0
- package/dist/options/options-type.d.ts +3 -1
- package/dist/qase.js +19 -1
- package/dist/reporters/abstract-reporter.d.ts +1 -1
- package/dist/reporters/index.d.ts +1 -0
- package/dist/reporters/index.js +3 -1
- package/dist/reporters/testops-multi-reporter.d.ts +50 -0
- package/dist/reporters/testops-multi-reporter.js +299 -0
- package/dist/reporters/testops-reporter.js +1 -1
- package/dist/utils/project-mapping-utils.d.ts +46 -0
- package/dist/utils/project-mapping-utils.js +86 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ All configuration options are listed in the table below:
|
|
|
34
34
|
| Description | Config file | Environment variable | Default value | Required | Possible values |
|
|
35
35
|
|-----------------------------------------------------------------------------------------------------------------------|----------------------------|---------------------------------|-----------------------------------------|----------|----------------------------|
|
|
36
36
|
| **Common** | | | | | |
|
|
37
|
-
| Mode of reporter | `mode` | `QASE_MODE` | `off` | No | `testops`, `report`, `off` |
|
|
38
|
-
| Fallback mode of reporter | `fallback` | `QASE_FALLBACK` | `off` | No | `testops`, `report`, `off` |
|
|
37
|
+
| Mode of reporter | `mode` | `QASE_MODE` | `off` | No | `testops`, `testops_multi`, `report`, `off` |
|
|
38
|
+
| Fallback mode of reporter | `fallback` | `QASE_FALLBACK` | `off` | No | `testops`, `testops_multi`, `report`, `off` |
|
|
39
39
|
| Environment | `environment` | `QASE_ENVIRONMENT` | undefined | No | Any string |
|
|
40
40
|
| Root suite | `rootSuite` | `QASE_ROOT_SUITE` | undefined | No | Any string |
|
|
41
41
|
| Enable debug logs | `debug` | `QASE_DEBUG` | `False` | No | `True`, `False` |
|
|
@@ -67,6 +67,18 @@ All configuration options are listed in the table below:
|
|
|
67
67
|
| Configuration values to create/find in groups (format: `group1=value1,group2=value2`) | `testops.configurations.values` | `QASE_TESTOPS_CONFIGURATIONS_VALUES` | undefined | No | Comma-separated key=value pairs |
|
|
68
68
|
| Create configuration groups if they don't exist | `testops.configurations.createIfNotExists` | `QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS` | `false` | No | `True`, `False` |
|
|
69
69
|
| Enable public report link generation and display after test run completion | `testops.showPublicReportLink` | `QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK` | `False` | No | `True`, `False` |
|
|
70
|
+
| **Qase TestOps Multi-Project configuration** | | | | | |
|
|
71
|
+
| Default project code for tests without explicit project mapping | `testops_multi.default_project` | N/A (use config file) | First project in `projects` | No | Any string (must match one of `projects[].code`) |
|
|
72
|
+
| Array of project configurations | `testops_multi.projects` | N/A (use config file) | `[]` | Yes\*\* | Array of objects |
|
|
73
|
+
| Project code | `testops_multi.projects[].code` | N/A | undefined | Yes\*\* | Any string |
|
|
74
|
+
| Project-specific test run title | `testops_multi.projects[].run.title` | N/A | Same as single-project default | No | Any string |
|
|
75
|
+
| Project-specific test run description | `testops_multi.projects[].run.description` | N/A | Same as single-project default | No | Any string |
|
|
76
|
+
| Project-specific test run complete | `testops_multi.projects[].run.complete` | N/A | `true` | No | `true`, `false` |
|
|
77
|
+
| Project-specific test run ID | `testops_multi.projects[].run.id` | N/A | undefined | No | Any integer |
|
|
78
|
+
| Project-specific test plan ID | `testops_multi.projects[].plan.id` | N/A | undefined | No | Any integer |
|
|
79
|
+
| Project-specific environment | `testops_multi.projects[].environment` | N/A | Uses global `environment` if not set | No | Any string |
|
|
80
|
+
|
|
81
|
+
\*\* Required when using `testops_multi` mode
|
|
70
82
|
|
|
71
83
|
### Example `qase.config.json` config
|
|
72
84
|
|
|
@@ -134,6 +146,64 @@ All configuration options are listed in the table below:
|
|
|
134
146
|
}
|
|
135
147
|
```
|
|
136
148
|
|
|
149
|
+
### Multi-Project Configuration (`testops_multi` mode)
|
|
150
|
+
|
|
151
|
+
Example `qase.config.json` for multi-project reporting:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mode": "testops_multi",
|
|
156
|
+
"fallback": "report",
|
|
157
|
+
"debug": false,
|
|
158
|
+
"environment": "local",
|
|
159
|
+
"logging": {
|
|
160
|
+
"console": true,
|
|
161
|
+
"file": false
|
|
162
|
+
},
|
|
163
|
+
"report": {
|
|
164
|
+
"driver": "local",
|
|
165
|
+
"connection": {
|
|
166
|
+
"local": {
|
|
167
|
+
"path": "./build/qase-report",
|
|
168
|
+
"format": "json"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"testops": {
|
|
173
|
+
"api": {
|
|
174
|
+
"token": "<token>",
|
|
175
|
+
"host": "qase.io"
|
|
176
|
+
},
|
|
177
|
+
"batch": { "size": 100 },
|
|
178
|
+
"showPublicReportLink": true
|
|
179
|
+
},
|
|
180
|
+
"testops_multi": {
|
|
181
|
+
"default_project": "PROJ1",
|
|
182
|
+
"projects": [
|
|
183
|
+
{
|
|
184
|
+
"code": "PROJ1",
|
|
185
|
+
"run": {
|
|
186
|
+
"title": "PROJ1 Multi-Project Run",
|
|
187
|
+
"description": "Test run for PROJ1 project",
|
|
188
|
+
"complete": true
|
|
189
|
+
},
|
|
190
|
+
"plan": { "id": 1 },
|
|
191
|
+
"environment": "staging"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"code": "PROJ2",
|
|
195
|
+
"run": {
|
|
196
|
+
"title": "PROJ2 Multi-Project Run",
|
|
197
|
+
"description": "Test run for PROJ2 project",
|
|
198
|
+
"complete": true
|
|
199
|
+
},
|
|
200
|
+
"environment": "production"
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
137
207
|
### Environment Variables Example
|
|
138
208
|
|
|
139
209
|
```bash
|
|
@@ -156,3 +226,193 @@ export QASE_LOGGING_FILE=true # Enable file output
|
|
|
156
226
|
# Enable public report link generation
|
|
157
227
|
export QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK=true
|
|
158
228
|
```
|
|
229
|
+
|
|
230
|
+
## Multi-Project Support
|
|
231
|
+
|
|
232
|
+
The multi-project feature allows you to send test results to multiple Qase projects simultaneously, with different test case IDs for each project. This is useful when:
|
|
233
|
+
|
|
234
|
+
* You need to report the same test to different projects
|
|
235
|
+
* Different projects track the same functionality with different test case IDs
|
|
236
|
+
* You want to maintain separate test runs for different environments or teams
|
|
237
|
+
|
|
238
|
+
### How It Works
|
|
239
|
+
|
|
240
|
+
1. Configure multiple projects in the `testops_multi.projects` array
|
|
241
|
+
2. Each project can have its own run configuration (title, description, plan, environment)
|
|
242
|
+
3. Use framework-specific helpers or markers to map test cases to projects (see table below)
|
|
243
|
+
4. Tests without explicit project mapping are sent to `default_project` (or the first project). Results without any case ID are also sent to the default project without linking to a test case.
|
|
244
|
+
|
|
245
|
+
### Framework-Specific Documentation
|
|
246
|
+
|
|
247
|
+
For detailed framework-specific documentation on multi-project support, see:
|
|
248
|
+
|
|
249
|
+
* **[Cypress Multi-Project Guide](../qase-cypress/docs/MULTI_PROJECT.md)** — `qase.projects(mapping, it(...))`, title markers
|
|
250
|
+
* **[Playwright Multi-Project Guide](../qase-playwright/docs/MULTI_PROJECT.md)** — `qase.projects(mapping)`, `qase.projectsTitle()`, annotations
|
|
251
|
+
* **[Jest Multi-Project Guide](../qase-jest/docs/MULTI_PROJECT.md)** — `qase.projects(mapping, name)`
|
|
252
|
+
* **[Vitest Multi-Project Guide](../qase-vitest/docs/MULTI_PROJECT.md)** — `addQaseProjects(name, mapping)`
|
|
253
|
+
* **[Mocha Multi-Project Guide](../qase-mocha/docs/MULTI_PROJECT.md)** — `qase.projects(mapping, name)`
|
|
254
|
+
* **[WDIO Multi-Project Guide](../qase-wdio/docs/MULTI_PROJECT.md)** — `qase.projects(mapping, name)`
|
|
255
|
+
* **[CucumberJS Multi-Project Guide](../qase-cucumberjs/docs/MULTI_PROJECT.md)** — tags `@qaseid.PROJ(ids)` in feature files
|
|
256
|
+
* **[Newman](../examples/multiProject/newman/README.md)** — comments in test script
|
|
257
|
+
* **[TestCafe](../examples/multiProject/testcafe/README.md)** — `qase.projects(mapping).create()` in `test.meta()`
|
|
258
|
+
|
|
259
|
+
### Example Usage
|
|
260
|
+
|
|
261
|
+
For runnable examples, see the [multi-project examples directory](../examples/multiProject/).
|
|
262
|
+
|
|
263
|
+
### Configuration
|
|
264
|
+
|
|
265
|
+
Set `mode` to `testops_multi` and add a `testops_multi` section with `default_project` and `projects`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"mode": "testops_multi",
|
|
270
|
+
"testops": {
|
|
271
|
+
"api": { "token": "<token>", "host": "qase.io" },
|
|
272
|
+
"batch": { "size": 100 }
|
|
273
|
+
},
|
|
274
|
+
"testops_multi": {
|
|
275
|
+
"default_project": "PROJ1",
|
|
276
|
+
"projects": [
|
|
277
|
+
{
|
|
278
|
+
"code": "PROJ1",
|
|
279
|
+
"run": { "title": "Project 1 Run", "complete": true },
|
|
280
|
+
"plan": { "id": 1 },
|
|
281
|
+
"environment": "staging"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"code": "PROJ2",
|
|
285
|
+
"run": { "title": "Project 2 Run", "id": 123, "complete": true },
|
|
286
|
+
"environment": "production"
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Test case mapping
|
|
294
|
+
|
|
295
|
+
For each result you can set `testops_project_mapping` (or use the helpers on `TestResultType`):
|
|
296
|
+
|
|
297
|
+
- **`testops_project_mapping`**: `Record<string, number[]>` — project code → list of test case IDs.
|
|
298
|
+
- **Backward compatibility**: if `testops_project_mapping` is empty or missing, the result uses `testops_id` and is sent to `default_project` (or the first project).
|
|
299
|
+
|
|
300
|
+
### How to specify projects and IDs in tests
|
|
301
|
+
|
|
302
|
+
Use the syntax below **only when `mode` is `testops_multi`**. For single-project mode (`testops`), keep using the usual single-project syntax (e.g. `(Qase ID: 123)` or `qase(1, 'name')`).
|
|
303
|
+
|
|
304
|
+
Use the **helper methods** below so project codes and IDs are formatted correctly (same idea as `qase(id, name)` for single-project).
|
|
305
|
+
|
|
306
|
+
#### Vitest
|
|
307
|
+
|
|
308
|
+
- **Single project:** `addQaseId(name, caseIds)` → `"name (Qase ID: 1,2)"`.
|
|
309
|
+
- **Multi-project:** `addQaseProjects(name, mapping)` → `"name (Qase PROJ1: 1,2) (Qase PROJ2: 3)"`.
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import { addQaseId, addQaseProjects } from 'vitest-qase-reporter/vitest';
|
|
313
|
+
|
|
314
|
+
// Single project (legacy)
|
|
315
|
+
it(addQaseId('login flow', [100]), async () => { ... });
|
|
316
|
+
|
|
317
|
+
// Multi-project — use helper so title is formatted correctly
|
|
318
|
+
it(addQaseProjects('login flow', { PROJ1: [100], PROJ2: [200] }), async () => { ... });
|
|
319
|
+
|
|
320
|
+
// Multiple IDs per project
|
|
321
|
+
it(addQaseProjects('checkout', { PROJ1: [10, 11], PROJ2: [20] }), async () => { ... });
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### Jest
|
|
325
|
+
|
|
326
|
+
- **Single project:** `qase(caseId, name)`.
|
|
327
|
+
- **Multi-project:** `qase.projects(mapping, name)`.
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
const { qase } = require('jest-qase-reporter');
|
|
331
|
+
|
|
332
|
+
// Single project
|
|
333
|
+
test(qase(100, 'login flow'), () => { ... });
|
|
334
|
+
|
|
335
|
+
// Multi-project
|
|
336
|
+
test(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'login flow'), () => { ... });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Cypress
|
|
340
|
+
|
|
341
|
+
- **Single project:** `qase(caseId, test)` (mutates test title) or title string `(Qase ID: 100)`.
|
|
342
|
+
- **Multi-project:** `qase.projects(mapping, name)` returns the title string; use it as the first argument to `it()`.
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const { qase } = require('cypress-qase-reporter');
|
|
346
|
+
|
|
347
|
+
// Single project
|
|
348
|
+
it(qase(100, 'login flow'), () => { ... }); // if qase receives test
|
|
349
|
+
it('login flow (Qase ID: 100)', () => { ... });
|
|
350
|
+
|
|
351
|
+
// Multi-project — use helper
|
|
352
|
+
it(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'login flow'), () => { ... });
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Tags (Cucumber/BDD):** `@qaseid.PROJ1(100) @qaseid.PROJ2(200)`.
|
|
356
|
+
|
|
357
|
+
#### Playwright
|
|
358
|
+
|
|
359
|
+
- **Single project:** `qase(caseId, name)` or `qase.id([1, 2])` in test.
|
|
360
|
+
- **Multi-project:** `qase.projects(mapping)` inside the test (metadata, not title).
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
const { test } = require('@playwright/test');
|
|
364
|
+
const { qase } = require('playwright-qase-reporter');
|
|
365
|
+
|
|
366
|
+
// Multi-project: set mapping inside the test
|
|
367
|
+
test('login and checkout', async ({ page }) => {
|
|
368
|
+
qase.projects({ PROJ1: [1, 2], PROJ2: [3] });
|
|
369
|
+
await page.goto('/login');
|
|
370
|
+
// ...
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Single project
|
|
374
|
+
test(qase(1, 'single project test'), async ({ page }) => { ... });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Mocha
|
|
378
|
+
|
|
379
|
+
- **Single project:** `qase(caseId, name)`.
|
|
380
|
+
- **Multi-project:** `qase.projects(mapping, name)`.
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
const { qase } = require('mocha-qase-reporter');
|
|
384
|
+
|
|
385
|
+
it(qase(100, 'login flow'), function () { ... });
|
|
386
|
+
it(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'login flow'), function () { ... });
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### WDIO (WebdriverIO)
|
|
390
|
+
|
|
391
|
+
- **Single project:** `qase(qaseId, name)`.
|
|
392
|
+
- **Multi-project:** `qase.projects(mapping, name)`.
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
const { qase } = require('wdio-qase-reporter');
|
|
396
|
+
|
|
397
|
+
it(qase(1, 'should work'), async () => { ... });
|
|
398
|
+
it(qase.projects({ PROJ1: [100], PROJ2: [200] }, 'Login flow'), async () => { ... });
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
You can still type markers manually in the title (e.g. `(Qase PROJ1: 100) (Qase PROJ2: 200)`); the helpers avoid typos and keep the format consistent.
|
|
402
|
+
|
|
403
|
+
#### Summary
|
|
404
|
+
|
|
405
|
+
| Framework | Single project (legacy) | Multi-project (project: IDs in title or API) |
|
|
406
|
+
|------------|-----------------------------|--------------------------------------------------|
|
|
407
|
+
| **Vitest** | `(Qase ID: 123)` in title | `(Qase PROJ1: 123) (Qase PROJ2: 456)` in title |
|
|
408
|
+
| **Jest** | `(Qase ID: 123)` in title | `(Qase PROJ1: 123) (Qase PROJ2: 456)` in title |
|
|
409
|
+
| **Cypress**| `(Qase ID: 123)` or `@qaseid(123)` | `(Qase PROJ1: 123) (Qase PROJ2: 456)` or `@qaseid.PROJ1(123) @qaseid.PROJ2(456)` |
|
|
410
|
+
| **Playwright** | `qase(1, 'name')` or metadata | `qase.projects({ PROJ1: [1, 2], PROJ2: [3] })` in test |
|
|
411
|
+
| **Mocha** | `(Qase ID: 123)` in title | `(Qase PROJ1: 123) (Qase PROJ2: 456)` in title |
|
|
412
|
+
| **WDIO** | `(Qase ID: 123)` in title | `(Qase PROJ1: 123) (Qase PROJ2: 456)` in title |
|
|
413
|
+
|
|
414
|
+
- **Title format:** `(Qase PROJECT_CODE: id1,id2,...)` — space after `Qase`, project code (letters, numbers, underscores), colon, then one or more IDs separated by commas. Several projects = several such blocks in one title.
|
|
415
|
+
- **Cypress tags:** `@qaseid.PROJECT_CODE(id1,id2,...)`.
|
|
416
|
+
- **Playwright:** `qase.projects({ PROJECT_CODE: [id1, id2, ...], ... })`; project codes must match `testops_multi.projects[].code`.
|
|
417
|
+
|
|
418
|
+
Tests that do not specify any multi-project mapping are sent to `default_project` (or the first project) using their usual single-project ID, if any.
|
package/changelog.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# qase-javascript-commons@2.5.0
|
|
2
|
+
|
|
3
|
+
## What's new
|
|
4
|
+
|
|
5
|
+
Added **multi-project support** (`testops_multi` mode):
|
|
6
|
+
|
|
7
|
+
- New mode `testops_multi` sends results to multiple Qase projects in one run; each project gets its own run.
|
|
8
|
+
- New field `testops_project_mapping` on `TestResultType`: project code → list of test case IDs. Helpers: `setTestopsProjectMapping()`, `getTestopsProjectMapping()`, `getTestopsIdsForProject()`, `getProjects()`.
|
|
9
|
+
- New config: `testops_multi.default_project` and `testops_multi.projects[]` with `code`, `run`, `plan`, `environment` per project.
|
|
10
|
+
- New reporter `TestOpsMultiReporter`; selected when `mode` is `testops_multi`. Single-project behavior is unchanged.
|
|
11
|
+
|
|
12
|
+
# qase-javascript-commons@2.4.18
|
|
13
|
+
|
|
14
|
+
## What's new
|
|
15
|
+
|
|
16
|
+
Fixed issue with incorrect link to failed test in the console output.
|
|
17
|
+
|
|
1
18
|
# qase-javascript-commons@2.4.17
|
|
2
19
|
|
|
3
20
|
## What's new
|
|
@@ -184,6 +184,88 @@ export declare const configValidationSchema: {
|
|
|
184
184
|
};
|
|
185
185
|
};
|
|
186
186
|
};
|
|
187
|
+
testops_multi: {
|
|
188
|
+
type: string;
|
|
189
|
+
nullable: boolean;
|
|
190
|
+
properties: {
|
|
191
|
+
default_project: {
|
|
192
|
+
type: string;
|
|
193
|
+
nullable: boolean;
|
|
194
|
+
};
|
|
195
|
+
projects: {
|
|
196
|
+
type: string;
|
|
197
|
+
items: {
|
|
198
|
+
type: string;
|
|
199
|
+
properties: {
|
|
200
|
+
code: {
|
|
201
|
+
type: string;
|
|
202
|
+
nullable: boolean;
|
|
203
|
+
};
|
|
204
|
+
run: {
|
|
205
|
+
type: string;
|
|
206
|
+
nullable: boolean;
|
|
207
|
+
properties: {
|
|
208
|
+
id: {
|
|
209
|
+
type: string;
|
|
210
|
+
nullable: boolean;
|
|
211
|
+
};
|
|
212
|
+
title: {
|
|
213
|
+
type: string;
|
|
214
|
+
nullable: boolean;
|
|
215
|
+
};
|
|
216
|
+
description: {
|
|
217
|
+
type: string;
|
|
218
|
+
nullable: boolean;
|
|
219
|
+
};
|
|
220
|
+
complete: {
|
|
221
|
+
type: string;
|
|
222
|
+
nullable: boolean;
|
|
223
|
+
};
|
|
224
|
+
tags: {
|
|
225
|
+
type: string;
|
|
226
|
+
items: {
|
|
227
|
+
type: string;
|
|
228
|
+
};
|
|
229
|
+
nullable: boolean;
|
|
230
|
+
};
|
|
231
|
+
externalLink: {
|
|
232
|
+
type: string;
|
|
233
|
+
nullable: boolean;
|
|
234
|
+
properties: {
|
|
235
|
+
type: {
|
|
236
|
+
type: string;
|
|
237
|
+
enum: ExternalLinkType[];
|
|
238
|
+
};
|
|
239
|
+
link: {
|
|
240
|
+
type: string;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
required: string[];
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
plan: {
|
|
248
|
+
type: string;
|
|
249
|
+
nullable: boolean;
|
|
250
|
+
properties: {
|
|
251
|
+
id: {
|
|
252
|
+
type: string;
|
|
253
|
+
nullable: boolean;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
environment: {
|
|
258
|
+
type: string;
|
|
259
|
+
nullable: boolean;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
required: string[];
|
|
263
|
+
};
|
|
264
|
+
nullable: boolean;
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
required: string[];
|
|
268
|
+
};
|
|
187
269
|
report: {
|
|
188
270
|
type: string;
|
|
189
271
|
nullable: boolean;
|
|
@@ -12,12 +12,12 @@ exports.configValidationSchema = {
|
|
|
12
12
|
properties: {
|
|
13
13
|
mode: {
|
|
14
14
|
type: 'string',
|
|
15
|
-
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.off],
|
|
15
|
+
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.testops_multi, options_1.ModeEnum.off],
|
|
16
16
|
nullable: true,
|
|
17
17
|
},
|
|
18
18
|
fallback: {
|
|
19
19
|
type: 'string',
|
|
20
|
-
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.off],
|
|
20
|
+
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.testops_multi, options_1.ModeEnum.off],
|
|
21
21
|
nullable: true,
|
|
22
22
|
},
|
|
23
23
|
debug: {
|
|
@@ -187,6 +187,69 @@ exports.configValidationSchema = {
|
|
|
187
187
|
},
|
|
188
188
|
},
|
|
189
189
|
},
|
|
190
|
+
testops_multi: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
nullable: true,
|
|
193
|
+
properties: {
|
|
194
|
+
default_project: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
nullable: true,
|
|
197
|
+
},
|
|
198
|
+
projects: {
|
|
199
|
+
type: 'array',
|
|
200
|
+
items: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
code: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
nullable: true,
|
|
206
|
+
},
|
|
207
|
+
run: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
nullable: true,
|
|
210
|
+
properties: {
|
|
211
|
+
id: { type: 'number', nullable: true },
|
|
212
|
+
title: { type: 'string', nullable: true },
|
|
213
|
+
description: { type: 'string', nullable: true },
|
|
214
|
+
complete: { type: 'boolean', nullable: true },
|
|
215
|
+
tags: {
|
|
216
|
+
type: 'array',
|
|
217
|
+
items: { type: 'string' },
|
|
218
|
+
nullable: true,
|
|
219
|
+
},
|
|
220
|
+
externalLink: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
nullable: true,
|
|
223
|
+
properties: {
|
|
224
|
+
type: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
enum: [TestOpsOptionsType_1.ExternalLinkType.JIRA_CLOUD, TestOpsOptionsType_1.ExternalLinkType.JIRA_SERVER],
|
|
227
|
+
},
|
|
228
|
+
link: { type: 'string' },
|
|
229
|
+
},
|
|
230
|
+
required: ['type', 'link'],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
plan: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
nullable: true,
|
|
237
|
+
properties: {
|
|
238
|
+
id: { type: 'number', nullable: true },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
environment: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
nullable: true,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ['code'],
|
|
247
|
+
},
|
|
248
|
+
nullable: true,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
required: ['projects'],
|
|
252
|
+
},
|
|
190
253
|
report: {
|
|
191
254
|
type: 'object',
|
|
192
255
|
nullable: true,
|
|
@@ -12,12 +12,12 @@ exports.envValidationSchema = {
|
|
|
12
12
|
properties: {
|
|
13
13
|
[env_enum_1.EnvEnum.mode]: {
|
|
14
14
|
type: 'string',
|
|
15
|
-
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.off],
|
|
15
|
+
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.testops_multi, options_1.ModeEnum.off],
|
|
16
16
|
nullable: true,
|
|
17
17
|
},
|
|
18
18
|
[env_enum_1.EnvEnum.fallback]: {
|
|
19
19
|
type: 'string',
|
|
20
|
-
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.off],
|
|
20
|
+
enum: [options_1.ModeEnum.report, options_1.ModeEnum.testops, options_1.ModeEnum.testops_multi, options_1.ModeEnum.off],
|
|
21
21
|
nullable: true,
|
|
22
22
|
},
|
|
23
23
|
[env_enum_1.EnvEnum.debug]: {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './reporters';
|
|
|
8
8
|
export * from './writer';
|
|
9
9
|
export * from './utils/get-package-version';
|
|
10
10
|
export * from './utils/mimeTypes';
|
|
11
|
+
export * from './utils/project-mapping-utils';
|
|
11
12
|
export * from './utils/signature';
|
|
12
13
|
export * from './utils/test-status-utils';
|
|
13
14
|
export * from './steps/step';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __exportStar(require("./reporters"), exports);
|
|
|
24
24
|
__exportStar(require("./writer"), exports);
|
|
25
25
|
__exportStar(require("./utils/get-package-version"), exports);
|
|
26
26
|
__exportStar(require("./utils/mimeTypes"), exports);
|
|
27
|
+
__exportStar(require("./utils/project-mapping-utils"), exports);
|
|
27
28
|
__exportStar(require("./utils/signature"), exports);
|
|
28
29
|
__exportStar(require("./utils/test-status-utils"), exports);
|
|
29
30
|
__exportStar(require("./steps/step"), exports);
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project configuration for multi-project (testops_multi) mode.
|
|
3
|
+
*/
|
|
4
|
+
export interface TestOpsProjectConfigType {
|
|
5
|
+
code: string;
|
|
6
|
+
run?: TestOpsRunType;
|
|
7
|
+
plan?: TestOpsPlanType;
|
|
8
|
+
environment?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Multi-project TestOps configuration.
|
|
12
|
+
*/
|
|
13
|
+
export interface TestOpsMultiConfigType {
|
|
14
|
+
/** Default project for tests without explicit mapping (and for results without any case ID). */
|
|
15
|
+
default_project?: string;
|
|
16
|
+
/** List of project configurations. */
|
|
17
|
+
projects: TestOpsProjectConfigType[];
|
|
18
|
+
}
|
|
1
19
|
export interface TestOpsOptionsType {
|
|
2
20
|
project: string;
|
|
3
21
|
uploadAttachments?: boolean | undefined;
|
package/dist/models/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { TestResultType } from './test-result';
|
|
2
|
-
export type { Relation, Suite, SuiteData } from './test-result';
|
|
2
|
+
export type { Relation, Suite, SuiteData, TestopsProjectMapping } from './test-result';
|
|
3
3
|
export { TestExecution, TestStatusEnum } from './test-execution';
|
|
4
4
|
export { TestStepType, StepType } from './test-step';
|
|
5
5
|
export { StepStatusEnum } from './step-execution';
|
|
@@ -8,4 +8,4 @@ export type { Report } from './report';
|
|
|
8
8
|
export { CompoundError } from './error';
|
|
9
9
|
export type { ConfigurationGroup, ConfigurationItem, ConfigurationGroupResponse } from './configuration';
|
|
10
10
|
export { ExternalLinkType } from './config/TestOpsOptionsType';
|
|
11
|
-
export type { TestOpsOptionsType, TestOpsApiType, TestOpsRunType, TestOpsPlanType, TestOpsBatchType, TestOpsConfigurationType, TestOpsConfigurationValueType, TestOpsExternalLinkType } from './config/TestOpsOptionsType';
|
|
11
|
+
export type { TestOpsOptionsType, TestOpsApiType, TestOpsRunType, TestOpsPlanType, TestOpsBatchType, TestOpsConfigurationType, TestOpsConfigurationValueType, TestOpsExternalLinkType, TestOpsProjectConfigType, TestOpsMultiConfigType } from './config/TestOpsOptionsType';
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { TestStepType } from './test-step';
|
|
2
2
|
import { Attachment } from './attachment';
|
|
3
3
|
import { TestExecution } from './test-execution';
|
|
4
|
+
/**
|
|
5
|
+
* Project code to test case IDs mapping for multi-project support.
|
|
6
|
+
* Key: project code (string), Value: array of test case IDs (numbers).
|
|
7
|
+
*/
|
|
8
|
+
export type TestopsProjectMapping = Record<string, number[]>;
|
|
4
9
|
export declare class TestResultType {
|
|
5
10
|
id: string;
|
|
6
11
|
title: string;
|
|
7
12
|
signature: string;
|
|
8
13
|
run_id: number | null;
|
|
9
14
|
testops_id: number | number[] | null;
|
|
15
|
+
/**
|
|
16
|
+
* Multi-project mapping: project code -> array of test case IDs.
|
|
17
|
+
* When set, overrides testops_id for multi-project mode.
|
|
18
|
+
* If empty/null, fall back to testops_id for single project.
|
|
19
|
+
*/
|
|
20
|
+
testops_project_mapping: TestopsProjectMapping | null;
|
|
10
21
|
execution: TestExecution;
|
|
11
22
|
fields: Record<string, string>;
|
|
12
23
|
attachments: Attachment[];
|
|
@@ -19,6 +30,22 @@ export declare class TestResultType {
|
|
|
19
30
|
message: string | null;
|
|
20
31
|
preparedAttachments?: string[];
|
|
21
32
|
constructor(title: string);
|
|
33
|
+
/**
|
|
34
|
+
* Set test case IDs for a specific project in multi-project mapping.
|
|
35
|
+
*/
|
|
36
|
+
setTestopsProjectMapping(projectCode: string, testopsIds: number[]): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get the entire project-to-IDs mapping.
|
|
39
|
+
*/
|
|
40
|
+
getTestopsProjectMapping(): TestopsProjectMapping | null;
|
|
41
|
+
/**
|
|
42
|
+
* Get test case IDs for a specific project.
|
|
43
|
+
*/
|
|
44
|
+
getTestopsIdsForProject(projectCode: string): number[] | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Get list of all project codes in the mapping.
|
|
47
|
+
*/
|
|
48
|
+
getProjects(): string[];
|
|
22
49
|
}
|
|
23
50
|
export interface Relation {
|
|
24
51
|
suite?: Suite;
|
|
@@ -8,6 +8,12 @@ class TestResultType {
|
|
|
8
8
|
signature;
|
|
9
9
|
run_id;
|
|
10
10
|
testops_id;
|
|
11
|
+
/**
|
|
12
|
+
* Multi-project mapping: project code -> array of test case IDs.
|
|
13
|
+
* When set, overrides testops_id for multi-project mode.
|
|
14
|
+
* If empty/null, fall back to testops_id for single project.
|
|
15
|
+
*/
|
|
16
|
+
testops_project_mapping;
|
|
11
17
|
execution;
|
|
12
18
|
fields;
|
|
13
19
|
attachments;
|
|
@@ -25,6 +31,7 @@ class TestResultType {
|
|
|
25
31
|
this.signature = '';
|
|
26
32
|
this.run_id = null;
|
|
27
33
|
this.testops_id = null;
|
|
34
|
+
this.testops_project_mapping = null;
|
|
28
35
|
this.execution = new test_execution_1.TestExecution();
|
|
29
36
|
this.fields = {};
|
|
30
37
|
this.attachments = [];
|
|
@@ -37,5 +44,32 @@ class TestResultType {
|
|
|
37
44
|
this.message = null;
|
|
38
45
|
this.preparedAttachments = [];
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Set test case IDs for a specific project in multi-project mapping.
|
|
49
|
+
*/
|
|
50
|
+
setTestopsProjectMapping(projectCode, testopsIds) {
|
|
51
|
+
if (!this.testops_project_mapping) {
|
|
52
|
+
this.testops_project_mapping = {};
|
|
53
|
+
}
|
|
54
|
+
this.testops_project_mapping[projectCode] = testopsIds;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the entire project-to-IDs mapping.
|
|
58
|
+
*/
|
|
59
|
+
getTestopsProjectMapping() {
|
|
60
|
+
return this.testops_project_mapping;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get test case IDs for a specific project.
|
|
64
|
+
*/
|
|
65
|
+
getTestopsIdsForProject(projectCode) {
|
|
66
|
+
return this.testops_project_mapping?.[projectCode];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get list of all project codes in the mapping.
|
|
70
|
+
*/
|
|
71
|
+
getProjects() {
|
|
72
|
+
return this.testops_project_mapping ? Object.keys(this.testops_project_mapping) : [];
|
|
73
|
+
}
|
|
40
74
|
}
|
|
41
75
|
exports.TestResultType = TestResultType;
|
|
@@ -8,5 +8,7 @@ var ModeEnum;
|
|
|
8
8
|
(function (ModeEnum) {
|
|
9
9
|
ModeEnum["report"] = "report";
|
|
10
10
|
ModeEnum["testops"] = "testops";
|
|
11
|
+
/** Multi-project mode: send results to multiple Qase projects. */
|
|
12
|
+
ModeEnum["testops_multi"] = "testops_multi";
|
|
11
13
|
ModeEnum["off"] = "off";
|
|
12
14
|
})(ModeEnum || (exports.ModeEnum = ModeEnum = {}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ModeEnum } from './mode-enum';
|
|
2
2
|
import { DriverEnum, FsWriterOptionsType } from '../writer';
|
|
3
|
-
import { TestOpsOptionsType } from '../models/config/TestOpsOptionsType';
|
|
3
|
+
import { TestOpsOptionsType, TestOpsMultiConfigType } from '../models/config/TestOpsOptionsType';
|
|
4
4
|
type RecursivePartial<T> = {
|
|
5
5
|
[K in keyof T]?: RecursivePartial<T[K]> | undefined;
|
|
6
6
|
};
|
|
@@ -28,6 +28,8 @@ export type OptionsType = {
|
|
|
28
28
|
statusMapping?: Record<string, string> | undefined;
|
|
29
29
|
logging?: RecursivePartial<LoggingOptionsType> | undefined;
|
|
30
30
|
testops?: RecursivePartial<TestOpsOptionsType> | undefined;
|
|
31
|
+
/** Multi-project configuration (used when mode is testops_multi). */
|
|
32
|
+
testops_multi?: TestOpsMultiConfigType | undefined;
|
|
31
33
|
report?: RecursivePartial<AdditionalReportOptionsType> | undefined;
|
|
32
34
|
};
|
|
33
35
|
export type FrameworkOptionsType<F extends string, O> = {
|
package/dist/qase.js
CHANGED
|
@@ -97,7 +97,10 @@ class QaseReporter {
|
|
|
97
97
|
this.logger.logDebug(`Config: ${JSON.stringify(this.sanitizeOptions(composedOptions))}`);
|
|
98
98
|
const effectiveMode = composedOptions.mode || options_1.ModeEnum.off;
|
|
99
99
|
const effectiveFallback = composedOptions.fallback || options_1.ModeEnum.off;
|
|
100
|
-
const needsHostData = effectiveMode === options_1.ModeEnum.testops ||
|
|
100
|
+
const needsHostData = effectiveMode === options_1.ModeEnum.testops ||
|
|
101
|
+
effectiveMode === options_1.ModeEnum.testops_multi ||
|
|
102
|
+
effectiveFallback === options_1.ModeEnum.testops ||
|
|
103
|
+
effectiveFallback === options_1.ModeEnum.testops_multi;
|
|
101
104
|
this.hostData = needsHostData
|
|
102
105
|
? (0, hostData_1.getHostInfo)(options.frameworkPackage, options.reporterName)
|
|
103
106
|
: (0, hostData_1.getMinimalHostData)();
|
|
@@ -453,6 +456,21 @@ class QaseReporter {
|
|
|
453
456
|
const apiClient = new clientV2_1.ClientV2(this.logger, options.testops, options.environment, options.rootSuite, this.hostData, options.reporterName, options.frameworkPackage);
|
|
454
457
|
return new reporters_1.TestOpsReporter(this.logger, apiClient, this.withState, options.testops.project, options.testops.api.host, options.testops.batch?.size, options.testops.run?.id, options.testops.showPublicReportLink);
|
|
455
458
|
}
|
|
459
|
+
case options_1.ModeEnum.testops_multi: {
|
|
460
|
+
if (!options.testops?.api?.token) {
|
|
461
|
+
throw new Error(`Either "testops.api.token" parameter or "${env_1.EnvApiEnum.token}" environment variable is required in "testops_multi" mode`);
|
|
462
|
+
}
|
|
463
|
+
const multi = options.testops_multi;
|
|
464
|
+
if (!multi?.projects?.length) {
|
|
465
|
+
throw new Error('"testops_multi.projects" must contain at least one project with a "code" field');
|
|
466
|
+
}
|
|
467
|
+
for (const p of multi.projects) {
|
|
468
|
+
if (!p?.code) {
|
|
469
|
+
throw new Error('Each project in "testops_multi.projects" must have a "code" field');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return new reporters_1.TestOpsMultiReporter(this.logger, options.testops, multi, this.withState, this.hostData, options.reporterName, options.frameworkPackage, options.environment, options.testops.api?.host, options.testops.batch?.size, options.testops.showPublicReportLink);
|
|
473
|
+
}
|
|
456
474
|
case options_1.ModeEnum.report: {
|
|
457
475
|
const localOptions = options.report?.connections?.[writer_1.DriverEnum.local];
|
|
458
476
|
const writer = new writer_1.FsWriter(localOptions);
|
|
@@ -64,5 +64,5 @@ export declare abstract class AbstractReporter implements InternalReporterInterf
|
|
|
64
64
|
* @param {TestResultType[]} results
|
|
65
65
|
*/
|
|
66
66
|
setTestResults(results: TestResultType[]): void;
|
|
67
|
-
|
|
67
|
+
protected removeAnsiEscapeCodes(str: string): string;
|
|
68
68
|
}
|
package/dist/reporters/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TestOpsReporter = exports.ReportReporter = exports.AbstractReporter = void 0;
|
|
3
|
+
exports.TestOpsMultiReporter = exports.TestOpsReporter = exports.ReportReporter = exports.AbstractReporter = void 0;
|
|
4
4
|
var abstract_reporter_1 = require("./abstract-reporter");
|
|
5
5
|
Object.defineProperty(exports, "AbstractReporter", { enumerable: true, get: function () { return abstract_reporter_1.AbstractReporter; } });
|
|
6
6
|
var report_reporter_1 = require("./report-reporter");
|
|
7
7
|
Object.defineProperty(exports, "ReportReporter", { enumerable: true, get: function () { return report_reporter_1.ReportReporter; } });
|
|
8
8
|
var testops_reporter_1 = require("./testops-reporter");
|
|
9
9
|
Object.defineProperty(exports, "TestOpsReporter", { enumerable: true, get: function () { return testops_reporter_1.TestOpsReporter; } });
|
|
10
|
+
var testops_multi_reporter_1 = require("./testops-multi-reporter");
|
|
11
|
+
Object.defineProperty(exports, "TestOpsMultiReporter", { enumerable: true, get: function () { return testops_multi_reporter_1.TestOpsMultiReporter; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AbstractReporter } from './abstract-reporter';
|
|
2
|
+
import { Attachment, TestResultType } from '../models';
|
|
3
|
+
import type { TestOpsOptionsType, TestOpsMultiConfigType } from '../models/config/TestOpsOptionsType';
|
|
4
|
+
import { LoggerInterface } from '../utils/logger';
|
|
5
|
+
import { HostData } from '../models/host-data';
|
|
6
|
+
/**
|
|
7
|
+
* Multi-project TestOps reporter. Sends test results to multiple Qase projects
|
|
8
|
+
* with different test case IDs per project. Each project gets its own run.
|
|
9
|
+
*/
|
|
10
|
+
export declare class TestOpsMultiReporter extends AbstractReporter {
|
|
11
|
+
private readonly baseUrl;
|
|
12
|
+
private readonly batchSize;
|
|
13
|
+
/** Project code -> run ID */
|
|
14
|
+
private readonly runIds;
|
|
15
|
+
/** Project code -> API client for that project */
|
|
16
|
+
private readonly clients;
|
|
17
|
+
/** Project code -> queue of results to send */
|
|
18
|
+
private readonly projectQueues;
|
|
19
|
+
/** Project code -> first unsent index (for batch tracking) */
|
|
20
|
+
private readonly firstIndexByProject;
|
|
21
|
+
private isTestRunReady;
|
|
22
|
+
private readonly mutex;
|
|
23
|
+
private readonly defaultProject;
|
|
24
|
+
private readonly projectCodes;
|
|
25
|
+
private readonly showPublicReportLink;
|
|
26
|
+
constructor(logger: LoggerInterface, testopsOptions: TestOpsOptionsType, multiConfig: TestOpsMultiConfigType, withState: boolean, hostData: HostData, reporterName: string | undefined, frameworkPackage: string | undefined, environment?: string, baseUrl?: string, batchSize?: number, showPublicReportLink?: boolean);
|
|
27
|
+
private buildProjectOptions;
|
|
28
|
+
startTestRun(): Promise<void>;
|
|
29
|
+
addTestResult(result: TestResultType): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get list of (projectCode, ids) for a result (multi-project mapping or legacy testops_id).
|
|
32
|
+
* Caller must hold mutex when using projectQueues.
|
|
33
|
+
*/
|
|
34
|
+
private getProjectsToUseForResult;
|
|
35
|
+
/**
|
|
36
|
+
* Push a result into project queues (by project / case IDs). Used by addTestResult and by
|
|
37
|
+
* sendResults() when results were set via setTestResults() (e.g. Cypress hooks in another process).
|
|
38
|
+
* Caller must hold mutex.
|
|
39
|
+
*/
|
|
40
|
+
private distributeResultToProjectQueues;
|
|
41
|
+
private copyResultForProject;
|
|
42
|
+
private checkOrCreateTestRuns;
|
|
43
|
+
private sendResultsForProject;
|
|
44
|
+
sendResults(): Promise<void>;
|
|
45
|
+
publish(): Promise<void>;
|
|
46
|
+
complete(): Promise<void>;
|
|
47
|
+
uploadAttachment(attachment: Attachment): Promise<string>;
|
|
48
|
+
private getBaseUrl;
|
|
49
|
+
private showLink;
|
|
50
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TestOpsMultiReporter = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const uuid_1 = require("uuid");
|
|
9
|
+
const abstract_reporter_1 = require("./abstract-reporter");
|
|
10
|
+
const models_1 = require("../models");
|
|
11
|
+
const async_mutex_1 = require("async-mutex");
|
|
12
|
+
const clientV2_1 = require("../client/clientV2");
|
|
13
|
+
const defaultChunkSize = 200;
|
|
14
|
+
/**
|
|
15
|
+
* Multi-project TestOps reporter. Sends test results to multiple Qase projects
|
|
16
|
+
* with different test case IDs per project. Each project gets its own run.
|
|
17
|
+
*/
|
|
18
|
+
class TestOpsMultiReporter extends abstract_reporter_1.AbstractReporter {
|
|
19
|
+
baseUrl;
|
|
20
|
+
batchSize;
|
|
21
|
+
/** Project code -> run ID */
|
|
22
|
+
runIds = new Map();
|
|
23
|
+
/** Project code -> API client for that project */
|
|
24
|
+
clients = new Map();
|
|
25
|
+
/** Project code -> queue of results to send */
|
|
26
|
+
projectQueues = new Map();
|
|
27
|
+
/** Project code -> first unsent index (for batch tracking) */
|
|
28
|
+
firstIndexByProject = new Map();
|
|
29
|
+
isTestRunReady = false;
|
|
30
|
+
mutex = new async_mutex_1.Mutex();
|
|
31
|
+
defaultProject;
|
|
32
|
+
projectCodes;
|
|
33
|
+
showPublicReportLink;
|
|
34
|
+
constructor(logger, testopsOptions, multiConfig, withState, hostData, reporterName, frameworkPackage, environment, baseUrl, batchSize, showPublicReportLink) {
|
|
35
|
+
super(logger);
|
|
36
|
+
this.baseUrl = this.getBaseUrl(baseUrl ?? testopsOptions.api?.host);
|
|
37
|
+
this.batchSize = batchSize ?? testopsOptions.batch?.size ?? defaultChunkSize;
|
|
38
|
+
this.showPublicReportLink = showPublicReportLink ?? testopsOptions.showPublicReportLink;
|
|
39
|
+
this.defaultProject =
|
|
40
|
+
multiConfig.default_project ??
|
|
41
|
+
(multiConfig.projects.length > 0 ? multiConfig.projects[0].code : '');
|
|
42
|
+
this.projectCodes = multiConfig.projects
|
|
43
|
+
.filter((p) => Boolean(p?.code))
|
|
44
|
+
.map((p) => p.code);
|
|
45
|
+
void withState; // reserved for future StateManager integration
|
|
46
|
+
for (const proj of multiConfig.projects) {
|
|
47
|
+
if (!proj?.code)
|
|
48
|
+
continue;
|
|
49
|
+
const projectOptions = this.buildProjectOptions(testopsOptions, proj);
|
|
50
|
+
const env = proj.environment ?? environment;
|
|
51
|
+
const client = new clientV2_1.ClientV2(logger, projectOptions, env, undefined, hostData, reporterName, frameworkPackage);
|
|
52
|
+
this.clients.set(proj.code, client);
|
|
53
|
+
this.projectQueues.set(proj.code, []);
|
|
54
|
+
this.firstIndexByProject.set(proj.code, 0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
buildProjectOptions(global, proj) {
|
|
58
|
+
const opts = {
|
|
59
|
+
project: proj.code,
|
|
60
|
+
api: global.api,
|
|
61
|
+
run: proj.run ?? global.run ?? {},
|
|
62
|
+
plan: proj.plan ?? global.plan ?? {},
|
|
63
|
+
uploadAttachments: global.uploadAttachments,
|
|
64
|
+
defect: global.defect,
|
|
65
|
+
configurations: global.configurations,
|
|
66
|
+
statusFilter: global.statusFilter,
|
|
67
|
+
showPublicReportLink: this.showPublicReportLink,
|
|
68
|
+
};
|
|
69
|
+
if (global.batch !== undefined) {
|
|
70
|
+
opts.batch = global.batch;
|
|
71
|
+
}
|
|
72
|
+
return opts;
|
|
73
|
+
}
|
|
74
|
+
async startTestRun() {
|
|
75
|
+
await this.checkOrCreateTestRuns();
|
|
76
|
+
}
|
|
77
|
+
async addTestResult(result) {
|
|
78
|
+
if (result.execution.status === models_1.TestStatusEnum.failed) {
|
|
79
|
+
const mapping = result.getTestopsProjectMapping?.() ?? result.testops_project_mapping ?? null;
|
|
80
|
+
if (mapping) {
|
|
81
|
+
for (const [projectCode, ids] of Object.entries(mapping)) {
|
|
82
|
+
for (const id of ids) {
|
|
83
|
+
this.showLink(projectCode, id, result.title);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const ids = Array.isArray(result.testops_id) ? result.testops_id : [result.testops_id];
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
if (id != null) {
|
|
91
|
+
this.showLink(this.defaultProject, id, result.title);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const release = await this.mutex.acquire();
|
|
97
|
+
try {
|
|
98
|
+
// Keep original in this.results for getTestResults / fallback
|
|
99
|
+
if (result.execution.stacktrace) {
|
|
100
|
+
result.execution.stacktrace = this.removeAnsiEscapeCodes(result.execution.stacktrace);
|
|
101
|
+
}
|
|
102
|
+
if (result.message) {
|
|
103
|
+
result.message = this.removeAnsiEscapeCodes(result.message);
|
|
104
|
+
}
|
|
105
|
+
this.results.push(result);
|
|
106
|
+
if (!this.isTestRunReady) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const { code, ids } of this.getProjectsToUseForResult(result)) {
|
|
110
|
+
const copy = this.copyResultForProject(result, code, ids);
|
|
111
|
+
const queue = this.projectQueues.get(code);
|
|
112
|
+
queue.push(copy);
|
|
113
|
+
const first = this.firstIndexByProject.get(code) ?? 0;
|
|
114
|
+
if (queue.length >= first + this.batchSize) {
|
|
115
|
+
await this.sendResultsForProject(code);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
release();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get list of (projectCode, ids) for a result (multi-project mapping or legacy testops_id).
|
|
125
|
+
* Caller must hold mutex when using projectQueues.
|
|
126
|
+
*/
|
|
127
|
+
getProjectsToUseForResult(result) {
|
|
128
|
+
const mapping = result.getTestopsProjectMapping?.() ?? result.testops_project_mapping ?? null;
|
|
129
|
+
const projectsToUse = [];
|
|
130
|
+
if (mapping && Object.keys(mapping).length > 0) {
|
|
131
|
+
for (const [code, ids] of Object.entries(mapping)) {
|
|
132
|
+
if (this.projectCodes.includes(code) && ids.length > 0) {
|
|
133
|
+
projectsToUse.push({ code, ids });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Backward compatibility: use default project + testops_id, or send without case ID to default project
|
|
139
|
+
const ids = Array.isArray(result.testops_id)
|
|
140
|
+
? result.testops_id
|
|
141
|
+
: result.testops_id != null
|
|
142
|
+
? [result.testops_id]
|
|
143
|
+
: [];
|
|
144
|
+
if (this.defaultProject) {
|
|
145
|
+
projectsToUse.push({ code: this.defaultProject, ids });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return projectsToUse;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Push a result into project queues (by project / case IDs). Used by addTestResult and by
|
|
152
|
+
* sendResults() when results were set via setTestResults() (e.g. Cypress hooks in another process).
|
|
153
|
+
* Caller must hold mutex.
|
|
154
|
+
*/
|
|
155
|
+
distributeResultToProjectQueues(result) {
|
|
156
|
+
for (const { code, ids } of this.getProjectsToUseForResult(result)) {
|
|
157
|
+
const copy = this.copyResultForProject(result, code, ids);
|
|
158
|
+
const queue = this.projectQueues.get(code);
|
|
159
|
+
queue.push(copy);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
copyResultForProject(result, _projectCode, ids) {
|
|
163
|
+
const copy = { ...result };
|
|
164
|
+
copy.id = (0, uuid_1.v4)();
|
|
165
|
+
copy.testops_id = ids.length === 0 ? null : ids.length === 1 ? ids[0] : ids;
|
|
166
|
+
copy.testops_project_mapping = null;
|
|
167
|
+
return copy;
|
|
168
|
+
}
|
|
169
|
+
async checkOrCreateTestRuns() {
|
|
170
|
+
for (const code of this.projectCodes) {
|
|
171
|
+
const client = this.clients.get(code);
|
|
172
|
+
const runId = await client.createRun();
|
|
173
|
+
this.runIds.set(code, runId);
|
|
174
|
+
this.logger.logDebug(`[${code}] Run ID: ${runId}`);
|
|
175
|
+
}
|
|
176
|
+
this.isTestRunReady = true;
|
|
177
|
+
}
|
|
178
|
+
async sendResultsForProject(projectCode) {
|
|
179
|
+
const queue = this.projectQueues.get(projectCode);
|
|
180
|
+
const first = this.firstIndexByProject.get(projectCode) ?? 0;
|
|
181
|
+
const client = this.clients.get(projectCode);
|
|
182
|
+
const runId = this.runIds.get(projectCode);
|
|
183
|
+
if (!queue || !client || runId === undefined) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const toSend = queue.slice(first, first + this.batchSize);
|
|
187
|
+
if (toSend.length === 0) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await client.uploadResults(runId, toSend);
|
|
192
|
+
this.firstIndexByProject.set(projectCode, first + toSend.length);
|
|
193
|
+
this.logger.logDebug(`[${projectCode}] Sent ${toSend.length} results to Qase`);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.logger.logError(`[${projectCode}] Error sending results:`, error);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async sendResults() {
|
|
202
|
+
const release = await this.mutex.acquire();
|
|
203
|
+
try {
|
|
204
|
+
// Only flush this.results when projectQueues are empty (e.g. Cypress: results set via
|
|
205
|
+
// setTestResults() in another process). When addTestResult() already queued results
|
|
206
|
+
// (same process, e.g. Cucumber), do not flush to avoid sending each result twice.
|
|
207
|
+
const queuesEmpty = this.projectCodes.every((code) => (this.projectQueues.get(code)?.length ?? 0) === 0);
|
|
208
|
+
if (this.results.length > 0 && this.isTestRunReady && queuesEmpty) {
|
|
209
|
+
for (const result of this.results) {
|
|
210
|
+
this.distributeResultToProjectQueues(result);
|
|
211
|
+
}
|
|
212
|
+
this.results = [];
|
|
213
|
+
}
|
|
214
|
+
for (const code of this.projectCodes) {
|
|
215
|
+
let sent;
|
|
216
|
+
do {
|
|
217
|
+
sent = await this.sendResultsForProject(code);
|
|
218
|
+
} while (sent);
|
|
219
|
+
}
|
|
220
|
+
for (const code of this.projectCodes) {
|
|
221
|
+
this.projectQueues.set(code, []);
|
|
222
|
+
this.firstIndexByProject.set(code, 0);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
release();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async publish() {
|
|
230
|
+
// Do not hold mutex here: sendResults() and complete() acquire it themselves.
|
|
231
|
+
// Holding mutex would deadlock when sendResults() tries to acquire the same mutex.
|
|
232
|
+
await this.sendResults();
|
|
233
|
+
await this.complete();
|
|
234
|
+
}
|
|
235
|
+
async complete() {
|
|
236
|
+
const release = await this.mutex.acquire();
|
|
237
|
+
try {
|
|
238
|
+
// Send any remaining results per project
|
|
239
|
+
for (const code of this.projectCodes) {
|
|
240
|
+
let sent;
|
|
241
|
+
do {
|
|
242
|
+
sent = await this.sendResultsForProject(code);
|
|
243
|
+
} while (sent);
|
|
244
|
+
}
|
|
245
|
+
for (const code of this.projectCodes) {
|
|
246
|
+
this.projectQueues.set(code, []);
|
|
247
|
+
this.firstIndexByProject.set(code, 0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
release();
|
|
252
|
+
}
|
|
253
|
+
const completePromises = this.projectCodes.map(async (code) => {
|
|
254
|
+
const client = this.clients.get(code);
|
|
255
|
+
const runId = this.runIds.get(code);
|
|
256
|
+
if (client && runId !== undefined) {
|
|
257
|
+
try {
|
|
258
|
+
await client.completeRun(runId);
|
|
259
|
+
if (this.showPublicReportLink) {
|
|
260
|
+
try {
|
|
261
|
+
await client.enablePublicReport(runId);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Error already logged in enablePublicReport
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.logger.log((0, chalk_1.default) `{green [${code}] Run ${runId} completed}`);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
this.logger.logError(`[${code}] Error completing run:`, error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
await Promise.all(completePromises);
|
|
275
|
+
}
|
|
276
|
+
async uploadAttachment(attachment) {
|
|
277
|
+
// Attachments are uploaded per project when results are sent; use default project's client
|
|
278
|
+
const client = this.clients.get(this.defaultProject);
|
|
279
|
+
if (client) {
|
|
280
|
+
return await client.uploadAttachment(attachment);
|
|
281
|
+
}
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
getBaseUrl(url) {
|
|
285
|
+
if (!url || url === 'qase.io') {
|
|
286
|
+
return 'https://app.qase.io';
|
|
287
|
+
}
|
|
288
|
+
return `https://${url.replace('api', 'app')}`;
|
|
289
|
+
}
|
|
290
|
+
showLink(projectCode, id, title) {
|
|
291
|
+
const runId = this.runIds.get(projectCode);
|
|
292
|
+
if (runId === undefined)
|
|
293
|
+
return;
|
|
294
|
+
const baseLink = `${this.baseUrl}/run/${projectCode}/dashboard/${runId}?source=logs&search=`;
|
|
295
|
+
const link = id != null ? `${baseLink}${projectCode}-${id}` : `${baseLink}${encodeURI(title)}`;
|
|
296
|
+
this.logger.log((0, chalk_1.default) `{blue See why this test failed: ${link}}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
exports.TestOpsMultiReporter = TestOpsMultiReporter;
|
|
@@ -181,7 +181,7 @@ class TestOpsReporter extends abstract_reporter_1.AbstractReporter {
|
|
|
181
181
|
if (!this.runId) {
|
|
182
182
|
throw new Error('Run ID is not set');
|
|
183
183
|
}
|
|
184
|
-
const baseLink = `${this.baseUrl}/run/${this.projectCode}/dashboard/${this.runId}?source=logs&
|
|
184
|
+
const baseLink = `${this.baseUrl}/run/${this.projectCode}/dashboard/${this.runId}?source=logs&search=`;
|
|
185
185
|
if (id) {
|
|
186
186
|
return `${baseLink}${this.projectCode}-${id}`;
|
|
187
187
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { TestopsProjectMapping } from '../models';
|
|
2
|
+
/**
|
|
3
|
+
* Result of parsing project/ID markers from a test title.
|
|
4
|
+
* - legacyIds: from "(Qase ID: 123)" or "(Qase ID: 123,124)" — single-project mode.
|
|
5
|
+
* - projectMapping: from "(Qase PROJ1: 123)" etc. — multi-project mode (project code -> IDs).
|
|
6
|
+
* - cleanedTitle: title with all such patterns removed.
|
|
7
|
+
*/
|
|
8
|
+
export interface ParsedProjectMapping {
|
|
9
|
+
legacyIds: number[];
|
|
10
|
+
projectMapping: TestopsProjectMapping;
|
|
11
|
+
cleanedTitle: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Result of parsing @qaseid / @qaseid.PROJ tags.
|
|
15
|
+
* - legacyIds: from @qaseid(123) or @qaseid(123,124).
|
|
16
|
+
* - projectMapping: from @qaseid.PROJ1(123,124).
|
|
17
|
+
*/
|
|
18
|
+
export interface ParsedTagsProjectMapping {
|
|
19
|
+
legacyIds: number[];
|
|
20
|
+
projectMapping: TestopsProjectMapping;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse @qaseid and @qaseid.PROJECT tags into legacy IDs and project mapping.
|
|
24
|
+
*
|
|
25
|
+
* @param tags — e.g. ["@qaseid(1,2)", "@qaseid.PROJ2(3)"]
|
|
26
|
+
* @returns legacyIds from @qaseid(...), projectMapping from @qaseid.PROJ(...)
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseProjectMappingFromTags(tags: string[]): ParsedTagsProjectMapping;
|
|
29
|
+
/**
|
|
30
|
+
* Parse multi-project and legacy Qase ID markers from a test title.
|
|
31
|
+
* - "(Qase ID: 123)" or "(Qase ID: 123,124)" → legacyIds, single-project.
|
|
32
|
+
* - "(Qase PROJ1: 123)" or "(Qase PROJ2: 456)" → projectMapping for testops_multi.
|
|
33
|
+
*
|
|
34
|
+
* @param title — test title that may contain "(Qase ID: …)" or "(Qase PROJECT_CODE: …)".
|
|
35
|
+
* @returns legacyIds, projectMapping, and cleanedTitle with all markers stripped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseProjectMappingFromTitle(title: string): ParsedProjectMapping;
|
|
38
|
+
/**
|
|
39
|
+
* Build a test title with multi-project markers for use in test names.
|
|
40
|
+
* Use this (or framework-specific qase.projects()) so the reporter can parse project and IDs.
|
|
41
|
+
*
|
|
42
|
+
* @param title — base test title (e.g. "Login flow").
|
|
43
|
+
* @param mapping — project code → list of test case IDs (e.g. { PROJ1: [1, 2], PROJ2: [3] }).
|
|
44
|
+
* @returns title with appended markers, e.g. "Login flow (Qase PROJ1: 1,2) (Qase PROJ2: 3)".
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatTitleWithProjectMapping(title: string, mapping: TestopsProjectMapping): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseProjectMappingFromTags = parseProjectMappingFromTags;
|
|
4
|
+
exports.parseProjectMappingFromTitle = parseProjectMappingFromTitle;
|
|
5
|
+
exports.formatTitleWithProjectMapping = formatTitleWithProjectMapping;
|
|
6
|
+
/** Matches @qaseid(123) or @qaseid.PROJ1(123,124). */
|
|
7
|
+
const QASEID_TAG_REGEXP = /@qaseid(?:\.([A-Za-z0-9_]+))?\(([\d,]+)\)/gi;
|
|
8
|
+
/**
|
|
9
|
+
* Parse @qaseid and @qaseid.PROJECT tags into legacy IDs and project mapping.
|
|
10
|
+
*
|
|
11
|
+
* @param tags — e.g. ["@qaseid(1,2)", "@qaseid.PROJ2(3)"]
|
|
12
|
+
* @returns legacyIds from @qaseid(...), projectMapping from @qaseid.PROJ(...)
|
|
13
|
+
*/
|
|
14
|
+
function parseProjectMappingFromTags(tags) {
|
|
15
|
+
const legacyIds = [];
|
|
16
|
+
const projectMapping = {};
|
|
17
|
+
for (const tag of tags) {
|
|
18
|
+
let m;
|
|
19
|
+
const re = new RegExp(QASEID_TAG_REGEXP.source, 'gi');
|
|
20
|
+
while ((m = re.exec(tag)) !== null) {
|
|
21
|
+
const projectCode = m[1]; // undefined for @qaseid(1)
|
|
22
|
+
const idsStr = m[2] ?? '';
|
|
23
|
+
const ids = idsStr.split(',').map((s) => parseInt(s, 10)).filter((n) => !Number.isNaN(n));
|
|
24
|
+
if (!projectCode || projectCode.toUpperCase() === 'ID') {
|
|
25
|
+
legacyIds.push(...ids);
|
|
26
|
+
}
|
|
27
|
+
else if (ids.length > 0) {
|
|
28
|
+
const existing = projectMapping[projectCode] ?? [];
|
|
29
|
+
projectMapping[projectCode] = [...existing, ...ids];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { legacyIds, projectMapping };
|
|
34
|
+
}
|
|
35
|
+
/** Matches "(Qase ID: 123)" or "(Qase PROJ1: 123,124)" — optional space after "Qase". */
|
|
36
|
+
const QASE_MARKER_REGEXP = /\(Qase\s+([A-Za-z0-9_]+):\s*([\d,]+)\)/gi;
|
|
37
|
+
/**
|
|
38
|
+
* Parse multi-project and legacy Qase ID markers from a test title.
|
|
39
|
+
* - "(Qase ID: 123)" or "(Qase ID: 123,124)" → legacyIds, single-project.
|
|
40
|
+
* - "(Qase PROJ1: 123)" or "(Qase PROJ2: 456)" → projectMapping for testops_multi.
|
|
41
|
+
*
|
|
42
|
+
* @param title — test title that may contain "(Qase ID: …)" or "(Qase PROJECT_CODE: …)".
|
|
43
|
+
* @returns legacyIds, projectMapping, and cleanedTitle with all markers stripped.
|
|
44
|
+
*/
|
|
45
|
+
function parseProjectMappingFromTitle(title) {
|
|
46
|
+
const legacyIds = [];
|
|
47
|
+
const projectMapping = {};
|
|
48
|
+
let cleanedTitle = title;
|
|
49
|
+
let m;
|
|
50
|
+
const re = new RegExp(QASE_MARKER_REGEXP.source, 'gi');
|
|
51
|
+
while ((m = re.exec(title)) !== null) {
|
|
52
|
+
const projectCode = m[1] ?? '';
|
|
53
|
+
const idsStr = m[2] ?? '';
|
|
54
|
+
const ids = idsStr.split(',').map((s) => parseInt(s, 10)).filter((n) => !Number.isNaN(n));
|
|
55
|
+
if (projectCode.toUpperCase() === 'ID') {
|
|
56
|
+
legacyIds.push(...ids);
|
|
57
|
+
}
|
|
58
|
+
else if (projectCode && ids.length > 0) {
|
|
59
|
+
const existing = projectMapping[projectCode] ?? [];
|
|
60
|
+
projectMapping[projectCode] = [...existing, ...ids];
|
|
61
|
+
}
|
|
62
|
+
cleanedTitle = cleanedTitle.replace(m[0], '');
|
|
63
|
+
}
|
|
64
|
+
cleanedTitle = cleanedTitle.replace(/\s+/g, ' ').trim();
|
|
65
|
+
return { legacyIds, projectMapping, cleanedTitle };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a test title with multi-project markers for use in test names.
|
|
69
|
+
* Use this (or framework-specific qase.projects()) so the reporter can parse project and IDs.
|
|
70
|
+
*
|
|
71
|
+
* @param title — base test title (e.g. "Login flow").
|
|
72
|
+
* @param mapping — project code → list of test case IDs (e.g. { PROJ1: [1, 2], PROJ2: [3] }).
|
|
73
|
+
* @returns title with appended markers, e.g. "Login flow (Qase PROJ1: 1,2) (Qase PROJ2: 3)".
|
|
74
|
+
*/
|
|
75
|
+
function formatTitleWithProjectMapping(title, mapping) {
|
|
76
|
+
if (!title || typeof title !== 'string') {
|
|
77
|
+
return title;
|
|
78
|
+
}
|
|
79
|
+
const parts = Object.entries(mapping)
|
|
80
|
+
.filter(([, ids]) => Array.isArray(ids) && ids.length > 0)
|
|
81
|
+
.map(([code, ids]) => `(Qase ${code}: ${ids.join(',')})`);
|
|
82
|
+
if (parts.length === 0) {
|
|
83
|
+
return title.trim();
|
|
84
|
+
}
|
|
85
|
+
return `${title.trim()} ${parts.join(' ')}`;
|
|
86
|
+
}
|