@virag8/azure-devops-test-publisher 1.0.1 → 1.2.2
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 +148 -26
- package/dist/azureService.d.ts +13 -1
- package/dist/azureService.js +131 -40
- package/dist/index.js +2 -0
- package/dist/types.d.ts +7 -1
- package/dist/wdioService.d.ts +12 -22
- package/dist/wdioService.js +31 -15
- package/package.json +64 -62
package/README.md
CHANGED
|
@@ -5,12 +5,17 @@ Publish automated test results and failure screenshots from WebdriverIO (Mocha o
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Single shared Test Run** — creates one Azure DevOps Test Run for the whole suite in the launcher process and shares its id with all parallel workers via an environment variable, so results from every spec land in the same run.
|
|
8
|
+
- **Only executed tests appear in the run** — the run is created empty and test points are attached as tests finish, so cases in the suite that never ran are not left sitting in an _In progress_ state.
|
|
8
9
|
- **Mocha support** — extracts the Azure DevOps test case id from a test title (e.g. `C1234 login works`) via the `afterTest` hook.
|
|
9
10
|
- **Cucumber / BDD support** — extracts the test case id from a scenario's `@C1234` tag, falling back to the scenario name, via the `afterScenario` hook.
|
|
10
11
|
- **Custom case id pattern** — override the default `C123`/`#123` matcher with your own regex (e.g. `TC-(\d+)`) via `caseIdPattern`.
|
|
11
12
|
- **Automatic failure screenshots** — captures a browser screenshot and attaches it to the Azure DevOps result whenever a test/scenario fails (toggle with `screenshotOnFailure`).
|
|
12
|
-
- **
|
|
13
|
+
- **Configuration-aware publishing** — scope a publish to a single Azure DevOps configuration (Android vs iOS, Chrome vs Firefox, …) so parallel jobs never overwrite each other's results for the same test case.
|
|
14
|
+
- **Run reuse** — publish into an existing run (`runId`), or keep a single run open across multiple publishes (`reuseTestRun` / `keepRunOpen`) instead of creating a new run every time. A run is only created when no run id is supplied — `0` counts as "not supplied".
|
|
15
|
+
- **"Run by" populated** — results are stamped with the identity that owns the PAT instead of showing an empty _Run by_ column.
|
|
13
16
|
- **Point pre-fetch** — pass already-fetched test points via `PublishOptions.points` to skip a redundant Azure DevOps API call.
|
|
17
|
+
- **Fail-fast option validation** — a missing `orgUrl`, `projectId`, `planId` or `suiteId` throws a typed `AzureDevOpsConfigError` listing every offending key, at construction time rather than mid-run.
|
|
18
|
+
- **Actionable diagnostics** — warnings name the project, plan, suite, configuration and the exact test case ids that could not be matched.
|
|
14
19
|
- **Resilient publishing** — publish failures are caught and logged so a flaky Azure DevOps API never fails the test run itself.
|
|
15
20
|
- **Standalone reporter service** — `AzureDevOpsReporterService` for custom/non-service integrations that just need `afterTest` + `onComplete` hooks.
|
|
16
21
|
- **Quiet by default** — set `debug: true` to log raw Azure DevOps API payloads while troubleshooting.
|
|
@@ -41,7 +46,7 @@ exports.config = {
|
|
|
41
46
|
{
|
|
42
47
|
orgUrl: process.env.AZURE_ORG_URL,
|
|
43
48
|
token: process.env.AZURE_PAT,
|
|
44
|
-
|
|
49
|
+
projectId: "MyProject", // name or GUID
|
|
45
50
|
planId: 123,
|
|
46
51
|
suiteId: 456,
|
|
47
52
|
screenshotOnFailure: true, // optional, defaults to true
|
|
@@ -85,6 +90,97 @@ If no tag is present, the case id is parsed from the scenario name instead.
|
|
|
85
90
|
|
|
86
91
|
The pattern must contain exactly one capturing group for the numeric id.
|
|
87
92
|
|
|
93
|
+
### Test configurations (Android vs iOS, Chrome vs Firefox, …)
|
|
94
|
+
|
|
95
|
+
If the same test case exists in your suite under several Azure DevOps _configurations_, tell each worker which configuration it represents. Without this, a result published for one configuration can overwrite another configuration's result for the same case.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
{
|
|
99
|
+
configurationId: Number(process.env.ADO_CONFIGURATION_ID), // e.g. 1042 = Android
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
With `configurationId` set, the service:
|
|
104
|
+
|
|
105
|
+
- attaches only the test point belonging to that configuration to the run, and
|
|
106
|
+
- updates only the result for that configuration, leaving the others untouched.
|
|
107
|
+
|
|
108
|
+
Run one WebdriverIO process per configuration:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ADO_CONFIGURATION_ID=1042 npx wdio run wdio.android.conf.ts
|
|
112
|
+
ADO_CONFIGURATION_ID=1043 npx wdio run wdio.ios.conf.ts
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Find the id under **Test Plans → Configurations**, or at `https://dev.azure.com/<org>/<project>/_apis/test/configurations`.
|
|
116
|
+
|
|
117
|
+
> While `configurationId` is set, points and results that have no configuration are skipped. Leave it unset for suites that don't use configurations.
|
|
118
|
+
|
|
119
|
+
### Reusing an existing Test Run
|
|
120
|
+
|
|
121
|
+
A run is created in `onPrepare` **only** when no run id is available. The id is looked up in this order:
|
|
122
|
+
|
|
123
|
+
1. the `AZURE_DEVOPS_TEST_RUN_ID` environment variable, then
|
|
124
|
+
2. the `runId` service option.
|
|
125
|
+
|
|
126
|
+
Both `0` and an unset value mean "create a new run". To publish into a run created elsewhere (e.g. by an earlier pipeline stage):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# PowerShell
|
|
130
|
+
$env:AZURE_DEVOPS_TEST_RUN_ID = "12345"; npx wdio run wdio.conf.ts
|
|
131
|
+
|
|
132
|
+
# bash
|
|
133
|
+
AZURE_DEVOPS_TEST_RUN_ID=12345 npx wdio run wdio.conf.ts
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The variable name is exported as a constant so you don't have to hardcode it:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { RUN_ID_ENV_VAR } from "@virag8/azure-devops-test-publisher";
|
|
140
|
+
|
|
141
|
+
console.log(`Publishing into run ${process.env[RUN_ID_ENV_VAR]}`);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Workers inherit the variable from the launcher process, so it is readable inside specs and hooks. It is cleared again in `onComplete` and does not propagate back to the shell that started WebdriverIO.
|
|
145
|
+
|
|
146
|
+
### Validating configuration early
|
|
147
|
+
|
|
148
|
+
`orgUrl`, `projectId`, `planId` and `suiteId` are mandatory. If any is missing or malformed, construction throws `AzureDevOpsConfigError` before a single test runs:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import {
|
|
152
|
+
AzureDevOpsService,
|
|
153
|
+
AzureDevOpsConfigError,
|
|
154
|
+
} from "@virag8/azure-devops-test-publisher";
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
new AzureDevOpsService(options);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof AzureDevOpsConfigError) {
|
|
160
|
+
console.error("Bad Azure DevOps config:", err.missing); // e.g. ["planId", "suiteId"]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
AzureDevOpsConfigError: Missing or invalid Azure DevOps option(s): planId, suiteId.
|
|
167
|
+
Provide them when constructing the service or in the wdio service options.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A missing `token` is **not** an error — it simply disables publishing with a warning, which keeps local runs working without a PAT.
|
|
171
|
+
|
|
172
|
+
### Troubleshooting unmatched test cases
|
|
173
|
+
|
|
174
|
+
When a case id from a title or tag has no matching test point, the exact ids are logged:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
No test point found for test case id(s) 9999 in project "MyProject", plan 123, suite 456, configuration 1042.
|
|
178
|
+
The suite exposes 12 point(s) for case id(s) 1001, 1002, … Check that the case ids in your test
|
|
179
|
+
titles belong to this plan/suite and configuration.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Common causes: the case lives in a different suite, the suite id belongs to another plan, or the worker's `configurationId` doesn't match the point's configuration. Set `debug: true` to also dump the raw API payloads.
|
|
183
|
+
|
|
88
184
|
## Usage as a standalone reporter
|
|
89
185
|
|
|
90
186
|
For custom runners that aren't WebdriverIO services, use `AzureDevOpsReporterService` directly:
|
|
@@ -95,7 +191,7 @@ import { AzureDevOpsReporterService } from "@virag8/azure-devops-test-publisher"
|
|
|
95
191
|
const reporter = new AzureDevOpsReporterService({
|
|
96
192
|
orgUrl: process.env.AZURE_ORG_URL!,
|
|
97
193
|
token: process.env.AZURE_PAT!,
|
|
98
|
-
|
|
194
|
+
projectId: "MyProject",
|
|
99
195
|
planId: 123,
|
|
100
196
|
suiteId: 456,
|
|
101
197
|
});
|
|
@@ -118,7 +214,7 @@ import { AzureDevOpsService } from "@virag8/azure-devops-test-publisher";
|
|
|
118
214
|
const ado = new AzureDevOpsService({
|
|
119
215
|
orgUrl: process.env.AZURE_ORG_URL!,
|
|
120
216
|
token: process.env.AZURE_PAT!,
|
|
121
|
-
|
|
217
|
+
projectId: "MyProject",
|
|
122
218
|
planId: 123,
|
|
123
219
|
suiteId: 456,
|
|
124
220
|
});
|
|
@@ -132,38 +228,62 @@ await ado.publishResults([
|
|
|
132
228
|
]);
|
|
133
229
|
```
|
|
134
230
|
|
|
231
|
+
Scope a publish to one configuration, and keep the run open for later batches:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const runId = await ado.createRun();
|
|
235
|
+
|
|
236
|
+
await ado.publishResults(
|
|
237
|
+
[{ testCaseId: 1234, outcome: "Failed", errorMessage: "boom" }],
|
|
238
|
+
{ runId, configurationId: 1042, keepRunOpen: true },
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
await ado.completeRun(runId);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`createRun()` creates an **empty** run; points are attached by `publishResults` as results come in, so cases that never executed stay out of the run.
|
|
245
|
+
|
|
135
246
|
## Configuration reference
|
|
136
247
|
|
|
137
248
|
### `AzureDevOpsOptions`
|
|
138
249
|
|
|
139
|
-
| Option | Type | Description
|
|
140
|
-
| --------------- | ---------- |
|
|
141
|
-
| `orgUrl` | `string` | Azure DevOps organization URL.
|
|
142
|
-
| `token` | `string` | Personal access token with Test Plan read/write permissions.
|
|
143
|
-
| `
|
|
144
|
-
| `planId` | `number` | Test plan id.
|
|
145
|
-
| `suiteId` | `number` | Test suite id within the plan.
|
|
146
|
-
| `runName` | `string?` | Custom name for created runs.
|
|
147
|
-
| `runId` | `number?` | Reuse this existing run instead of creating a new one.
|
|
148
|
-
| `reuseTestRun` | `boolean?` | Keep a single run open across multiple `publishResults` calls.
|
|
149
|
-
| `caseIdPattern` | `RegExp?` | Custom regex (one capturing group) for extracting the test case id.
|
|
150
|
-
| `debug` | `boolean?` | Log raw Azure DevOps API payloads. Defaults to `false`.
|
|
151
|
-
|
|
152
|
-
> **Note on `
|
|
250
|
+
| Option | Type | Description |
|
|
251
|
+
| --------------- | ---------- | ------------------------------------------------------------------------------------ |
|
|
252
|
+
| `orgUrl` | `string` | Azure DevOps organization URL. |
|
|
253
|
+
| `token` | `string` | Personal access token with Test Plan read/write permissions. |
|
|
254
|
+
| `projectId` | `string` | Azure DevOps project **name or id (GUID)** — both are accepted. |
|
|
255
|
+
| `planId` | `number` | Test plan id. |
|
|
256
|
+
| `suiteId` | `number` | Test suite id within the plan. |
|
|
257
|
+
| `runName` | `string?` | Custom name for created runs. |
|
|
258
|
+
| `runId` | `number?` | Reuse this existing run instead of creating a new one. `0` means "create a new run". |
|
|
259
|
+
| `reuseTestRun` | `boolean?` | Keep a single run open across multiple `publishResults` calls. |
|
|
260
|
+
| `caseIdPattern` | `RegExp?` | Custom regex (one capturing group) for extracting the test case id. |
|
|
261
|
+
| `debug` | `boolean?` | Log raw Azure DevOps API payloads. Defaults to `false`. |
|
|
262
|
+
|
|
263
|
+
> **Note on `projectId`** — despite the name, this accepts either the project's display name (`"MyProject"`) or its GUID (`"b9e8c7cb-..."`). Prefer the GUID: it stays stable if the project is ever renamed, and it avoids URL-encoding issues with names that contain spaces. You can find it at `https://dev.azure.com/<org>/_apis/projects`.
|
|
153
264
|
|
|
154
265
|
### `AzureDevOpsWdioOptions` (extends `AzureDevOpsOptions`)
|
|
155
266
|
|
|
156
|
-
| Option | Type | Description
|
|
157
|
-
| --------------------- | ---------- |
|
|
158
|
-
| `screenshotOnFailure` | `boolean?` | Attach a browser screenshot to failed results. Defaults to `true`.
|
|
267
|
+
| Option | Type | Description |
|
|
268
|
+
| --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
269
|
+
| `screenshotOnFailure` | `boolean?` | Attach a browser screenshot to failed results. Defaults to `true`. |
|
|
270
|
+
| `configurationId` | `number?` | Azure DevOps test configuration id for this worker (e.g. Android vs iOS). Set this when the same test case is configured for multiple configurations in your suite, otherwise a result published for one configuration can overwrite another configuration's result for the same case. |
|
|
159
271
|
|
|
160
272
|
### `PublishOptions`
|
|
161
273
|
|
|
162
|
-
| Option
|
|
163
|
-
|
|
|
164
|
-
| `runId`
|
|
165
|
-
| `points`
|
|
166
|
-
| `
|
|
274
|
+
| Option | Type | Description |
|
|
275
|
+
| ----------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
276
|
+
| `runId` | `number?` | Publish into this existing run instead of creating a new one. `0` is treated as "not set". |
|
|
277
|
+
| `points` | `TestPoint[]?` | Reuse already-fetched test points instead of calling `getPoints` again. |
|
|
278
|
+
| `configurationId` | `number?` | Only publish to the test point/result of this configuration id; points and results in others are skipped. |
|
|
279
|
+
| `keepRunOpen` | `boolean?` | Leave the run in progress so more results can be added later. |
|
|
280
|
+
|
|
281
|
+
### `AzureDevOpsConfigError`
|
|
282
|
+
|
|
283
|
+
| Member | Type | Description |
|
|
284
|
+
| --------- | ---------- | ------------------------------------------------------- |
|
|
285
|
+
| `missing` | `string[]` | The mandatory option keys that were missing or invalid. |
|
|
286
|
+
| `message` | `string` | Human readable summary listing every offending key. |
|
|
167
287
|
|
|
168
288
|
## Development
|
|
169
289
|
|
|
@@ -173,6 +293,8 @@ npm run test:real # integration tests against a real Azure DevOps org (require
|
|
|
173
293
|
npm run build # compile to dist/
|
|
174
294
|
```
|
|
175
295
|
|
|
296
|
+
`npm run test:real` reads its credentials from a local `.env` file (`AZURE_ORG_URL`, `AZURE_PAT`, `AZURE_PROJECT`, `AZURE_PLAN_ID`, `AZURE_SUITE_ID`, `AZURE_TEST_CASE_ID`) and skips itself when they are absent.
|
|
297
|
+
|
|
176
298
|
## License
|
|
177
299
|
|
|
178
300
|
MIT
|
package/dist/azureService.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { AzureDevOpsOptions, PublishOptions, TestResultItem } from "./types";
|
|
2
|
+
/** Thrown when mandatory Azure DevOps options are missing or malformed. */
|
|
3
|
+
export declare class AzureDevOpsConfigError extends Error {
|
|
4
|
+
readonly missing: string[];
|
|
5
|
+
constructor(missing: string[]);
|
|
6
|
+
}
|
|
7
|
+
export declare function assertRequiredOptions(config: AzureDevOpsOptions): void;
|
|
2
8
|
export declare class AzureDevOpsService {
|
|
3
9
|
private testApiPromise?;
|
|
10
|
+
private connection?;
|
|
11
|
+
private runByPromise?;
|
|
4
12
|
private config;
|
|
5
13
|
private currentRunId?;
|
|
6
14
|
/** Whether a PAT was provided; when false, every public method is a no-op. */
|
|
@@ -8,8 +16,12 @@ export declare class AzureDevOpsService {
|
|
|
8
16
|
constructor(config: AzureDevOpsOptions);
|
|
9
17
|
/** Id of the run currently being published to, if any. */
|
|
10
18
|
get runId(): number | undefined;
|
|
19
|
+
/** PAT owner, surfaced as "Run by" on the result; Azure leaves the field blank otherwise. */
|
|
20
|
+
private getRunBy;
|
|
11
21
|
private debug;
|
|
12
|
-
/**
|
|
22
|
+
/** Human readable target used in warnings and errors. */
|
|
23
|
+
private describeTarget;
|
|
24
|
+
/** Creates an empty run; points are added by `publishResults` as tests finish, so unexecuted cases are never marked in progress. */
|
|
13
25
|
createRun(): Promise<number | undefined>;
|
|
14
26
|
publishResults(results: TestResultItem[], options?: PublishOptions): Promise<number | undefined>;
|
|
15
27
|
/** Marks a run as completed. Defaults to the run used by the last publish. */
|
package/dist/azureService.js
CHANGED
|
@@ -33,15 +33,56 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AzureDevOpsService = void 0;
|
|
36
|
+
exports.AzureDevOpsService = exports.AzureDevOpsConfigError = void 0;
|
|
37
|
+
exports.assertRequiredOptions = assertRequiredOptions;
|
|
37
38
|
const azdev = __importStar(require("azure-devops-node-api"));
|
|
39
|
+
/** True if `point`/`result` belongs to the same test case as `item`, and the same configuration when `item.configurationId` is set. */
|
|
40
|
+
function matchesTestCase(point, item) {
|
|
41
|
+
if (!point.testCase?.id ||
|
|
42
|
+
parseInt(point.testCase.id, 10) !== item.testCaseId) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (item.configurationId == null)
|
|
46
|
+
return true;
|
|
47
|
+
const pointConfigId = point.configuration?.id
|
|
48
|
+
? parseInt(point.configuration.id, 10)
|
|
49
|
+
: undefined;
|
|
50
|
+
return pointConfigId === item.configurationId;
|
|
51
|
+
}
|
|
52
|
+
/** Thrown when mandatory Azure DevOps options are missing or malformed. */
|
|
53
|
+
class AzureDevOpsConfigError extends Error {
|
|
54
|
+
missing;
|
|
55
|
+
constructor(missing) {
|
|
56
|
+
super(`Missing or invalid Azure DevOps option(s): ${missing.join(", ")}. ` +
|
|
57
|
+
"Provide them when constructing the service or in the wdio service options.");
|
|
58
|
+
this.missing = missing;
|
|
59
|
+
this.name = "AzureDevOpsConfigError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.AzureDevOpsConfigError = AzureDevOpsConfigError;
|
|
63
|
+
function assertRequiredOptions(config) {
|
|
64
|
+
const missing = [];
|
|
65
|
+
if (!config?.orgUrl?.trim())
|
|
66
|
+
missing.push("orgUrl");
|
|
67
|
+
if (!config?.projectId?.toString().trim())
|
|
68
|
+
missing.push("projectId");
|
|
69
|
+
if (!Number.isInteger(config?.planId))
|
|
70
|
+
missing.push("planId");
|
|
71
|
+
if (!Number.isInteger(config?.suiteId))
|
|
72
|
+
missing.push("suiteId");
|
|
73
|
+
if (missing.length > 0)
|
|
74
|
+
throw new AzureDevOpsConfigError(missing);
|
|
75
|
+
}
|
|
38
76
|
class AzureDevOpsService {
|
|
39
77
|
testApiPromise;
|
|
78
|
+
connection;
|
|
79
|
+
runByPromise;
|
|
40
80
|
config;
|
|
41
81
|
currentRunId;
|
|
42
82
|
/** Whether a PAT was provided; when false, every public method is a no-op. */
|
|
43
83
|
enabled;
|
|
44
84
|
constructor(config) {
|
|
85
|
+
assertRequiredOptions(config);
|
|
45
86
|
this.config = config;
|
|
46
87
|
this.currentRunId = config.runId;
|
|
47
88
|
this.enabled = Boolean(config.token);
|
|
@@ -51,39 +92,58 @@ class AzureDevOpsService {
|
|
|
51
92
|
}
|
|
52
93
|
const authHandler = azdev.getPersonalAccessTokenHandler(config.token);
|
|
53
94
|
const connection = new azdev.WebApi(config.orgUrl, authHandler);
|
|
95
|
+
this.connection = connection;
|
|
54
96
|
this.testApiPromise = connection.getTestApi();
|
|
55
97
|
}
|
|
56
98
|
/** Id of the run currently being published to, if any. */
|
|
57
99
|
get runId() {
|
|
58
100
|
return this.currentRunId;
|
|
59
101
|
}
|
|
102
|
+
/** PAT owner, surfaced as "Run by" on the result; Azure leaves the field blank otherwise. */
|
|
103
|
+
async getRunBy() {
|
|
104
|
+
try {
|
|
105
|
+
this.runByPromise ??= this.connection.connect().then((data) => data.authenticatedUser && {
|
|
106
|
+
id: data.authenticatedUser.id,
|
|
107
|
+
displayName: data.authenticatedUser.customDisplayName ||
|
|
108
|
+
data.authenticatedUser.providerDisplayName,
|
|
109
|
+
});
|
|
110
|
+
return await this.runByPromise;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.debug("Failed to resolve the Run by identity:", err);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
60
117
|
debug(message, payload) {
|
|
61
118
|
if (!this.config.debug)
|
|
62
119
|
return;
|
|
63
120
|
console.log(message, payload);
|
|
64
121
|
}
|
|
65
|
-
/**
|
|
122
|
+
/** Human readable target used in warnings and errors. */
|
|
123
|
+
describeTarget(configurationId) {
|
|
124
|
+
const parts = [
|
|
125
|
+
`project "${this.config.projectId}"`,
|
|
126
|
+
`plan ${this.config.planId}`,
|
|
127
|
+
`suite ${this.config.suiteId}`,
|
|
128
|
+
];
|
|
129
|
+
if (configurationId != null)
|
|
130
|
+
parts.push(`configuration ${configurationId}`);
|
|
131
|
+
return parts.join(", ");
|
|
132
|
+
}
|
|
133
|
+
/** Creates an empty run; points are added by `publishResults` as tests finish, so unexecuted cases are never marked in progress. */
|
|
66
134
|
async createRun() {
|
|
67
135
|
if (!this.enabled)
|
|
68
136
|
return undefined;
|
|
69
137
|
const testApi = await this.testApiPromise;
|
|
70
|
-
const points = await testApi.getPoints(this.config.projectName, this.config.planId, this.config.suiteId);
|
|
71
|
-
const pointIds = points
|
|
72
|
-
.map((p) => p.id)
|
|
73
|
-
.filter((id) => typeof id === "number");
|
|
74
|
-
const configurationIds = Array.from(new Set(points
|
|
75
|
-
.map((p) => p.configuration?.id ? parseInt(p.configuration.id, 10) : null)
|
|
76
|
-
.filter((id) => id !== null)));
|
|
77
138
|
const testRun = await testApi.createTestRun({
|
|
78
139
|
name: this.config.runName ||
|
|
79
140
|
`Automated Test Run - ${new Date().toISOString()}`,
|
|
80
141
|
automated: true,
|
|
81
142
|
plan: { id: this.config.planId.toString() },
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}, this.config.projectName);
|
|
143
|
+
configurationIds: [],
|
|
144
|
+
}, this.config.projectId);
|
|
85
145
|
if (!testRun.id) {
|
|
86
|
-
throw new Error(
|
|
146
|
+
throw new Error(`Failed to create Test Run in Azure DevOps for ${this.describeTarget()}; the API returned a run without an id.`);
|
|
87
147
|
}
|
|
88
148
|
this.currentRunId = testRun.id;
|
|
89
149
|
return testRun.id;
|
|
@@ -94,15 +154,29 @@ class AzureDevOpsService {
|
|
|
94
154
|
const testApi = await this.testApiPromise;
|
|
95
155
|
// 1. Get test points matching the local test cases
|
|
96
156
|
const points = options.points ??
|
|
97
|
-
(await testApi.getPoints(this.config.
|
|
98
|
-
this.debug("Fetched test points:", points);
|
|
157
|
+
(await testApi.getPoints(this.config.projectId, this.config.planId, this.config.suiteId));
|
|
158
|
+
this.debug("Fetched test points:", points.length);
|
|
159
|
+
const inTargetConfiguration = (item) => options.configurationId == null ||
|
|
160
|
+
(item.configuration?.id != null &&
|
|
161
|
+
parseInt(item.configuration.id, 10) === options.configurationId);
|
|
99
162
|
const targetCaseIds = new Set(results.map((r) => r.testCaseId));
|
|
100
|
-
const matchedPoints = points.filter((p) => p.testCase?.id &&
|
|
163
|
+
const matchedPoints = points.filter((p) => p.testCase?.id &&
|
|
164
|
+
targetCaseIds.has(parseInt(p.testCase.id, 10)) &&
|
|
165
|
+
inTargetConfiguration(p) &&
|
|
166
|
+
results.some((r) => matchesTestCase(p, r)));
|
|
101
167
|
this.debug("Matched test points:", matchedPoints);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
168
|
+
const matchedCaseIds = new Set(matchedPoints.map((p) => parseInt(p.testCase.id, 10)));
|
|
169
|
+
const unmatchedCaseIds = [...targetCaseIds].filter((id) => !matchedCaseIds.has(id));
|
|
170
|
+
if (unmatchedCaseIds.length > 0) {
|
|
171
|
+
console.warn(`No test point found for test case id(s) ${unmatchedCaseIds.join(", ")} in ${this.describeTarget(options.configurationId)}. ` +
|
|
172
|
+
`The suite exposes ${points.length} point(s) for case id(s) ${points
|
|
173
|
+
.map((p) => p.testCase?.id)
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join(", ") || "none"}. ` +
|
|
176
|
+
"Check that the case ids in your test titles belong to this plan/suite and configuration.");
|
|
105
177
|
}
|
|
178
|
+
if (matchedPoints.length === 0)
|
|
179
|
+
return;
|
|
106
180
|
const pointIds = matchedPoints.map((p) => p.id);
|
|
107
181
|
this.debug("Point IDs for the test run:", pointIds);
|
|
108
182
|
// Extract unique configuration IDs from matched points (or fallback to empty array/default)
|
|
@@ -117,18 +191,22 @@ class AzureDevOpsService {
|
|
|
117
191
|
let runId;
|
|
118
192
|
if (reusedRunId) {
|
|
119
193
|
runId = reusedRunId;
|
|
120
|
-
// Only add points that the run does not already hold a result for
|
|
121
|
-
const existingResults = await testApi.getTestResults(this.config.
|
|
122
|
-
const
|
|
123
|
-
const missingPoints = matchedPoints.filter((p) => !
|
|
194
|
+
// Only add points that the run does not already hold a result for (per case+configuration pair)
|
|
195
|
+
const existingResults = await testApi.getTestResults(this.config.projectId, runId);
|
|
196
|
+
const existingPointKeys = new Set(existingResults.map((r) => `${r.testCase?.id}:${r.configuration?.id ?? ""}`));
|
|
197
|
+
const missingPoints = matchedPoints.filter((p) => !existingPointKeys.has(`${p.testCase?.id}:${p.configuration?.id ?? ""}`));
|
|
124
198
|
if (missingPoints.length > 0) {
|
|
125
|
-
await testApi.addTestResultsToTestRun(
|
|
199
|
+
await testApi.addTestResultsToTestRun(
|
|
200
|
+
// Planned results are rejected unless point id, case id, revision and title are all present.
|
|
201
|
+
missingPoints.map((p) => ({
|
|
126
202
|
testPoint: { id: p.id.toString() },
|
|
127
203
|
testCase: { id: p.testCase.id },
|
|
204
|
+
testCaseRevision: 1,
|
|
205
|
+
testCaseTitle: p.testCase?.name || `Test case ${p.testCase.id}`,
|
|
128
206
|
configuration: p.configuration?.id
|
|
129
207
|
? { id: p.configuration.id }
|
|
130
208
|
: undefined,
|
|
131
|
-
})), this.config.
|
|
209
|
+
})), this.config.projectId, runId);
|
|
132
210
|
}
|
|
133
211
|
}
|
|
134
212
|
else {
|
|
@@ -140,49 +218,62 @@ class AzureDevOpsService {
|
|
|
140
218
|
plan: { id: this.config.planId.toString() },
|
|
141
219
|
pointIds: pointIds,
|
|
142
220
|
configurationIds: configurationIds,
|
|
143
|
-
}, this.config.
|
|
221
|
+
}, this.config.projectId);
|
|
144
222
|
if (!testRun.id) {
|
|
145
|
-
throw new Error(
|
|
223
|
+
throw new Error(`Failed to create Test Run in Azure DevOps for ${this.describeTarget(options.configurationId)} with point id(s) ${pointIds.join(", ")}.`);
|
|
146
224
|
}
|
|
147
225
|
runId = testRun.id;
|
|
148
226
|
}
|
|
149
227
|
this.currentRunId = runId;
|
|
150
228
|
// 3. Fetch automatically created results for the run
|
|
151
|
-
const runResults = await testApi.getTestResults(this.config.
|
|
229
|
+
const runResults = await testApi.getTestResults(this.config.projectId, runId);
|
|
152
230
|
// 4. Map outcomes and error messages
|
|
153
231
|
this.debug("Run results fetched from Azure DevOps:", runResults);
|
|
232
|
+
const runBy = await this.getRunBy();
|
|
154
233
|
const updatedResults = runResults
|
|
155
|
-
.filter((result) =>
|
|
234
|
+
.filter((result) => inTargetConfiguration(result) &&
|
|
235
|
+
results.some((r) => matchesTestCase(result, r)))
|
|
156
236
|
.map((result) => {
|
|
157
|
-
const match = results.find((r) =>
|
|
237
|
+
const match = results.find((r) => matchesTestCase(result, r));
|
|
158
238
|
return {
|
|
159
239
|
...result,
|
|
160
240
|
outcome: match ? match.outcome : "Inconclusive",
|
|
161
241
|
errorMessage: match?.errorMessage || "",
|
|
162
242
|
state: "Completed",
|
|
163
243
|
durationInMs: match?.durationInMs || 0,
|
|
244
|
+
runBy: result.runBy ?? runBy,
|
|
164
245
|
};
|
|
165
246
|
});
|
|
166
247
|
// 5. Update test results in ADO
|
|
167
248
|
this.debug("Updating test results in Azure DevOps:", updatedResults);
|
|
168
|
-
const savedResults = await testApi.updateTestResults(updatedResults, this.config.
|
|
249
|
+
const savedResults = await testApi.updateTestResults(updatedResults, this.config.projectId, runId);
|
|
169
250
|
this.debug("Saved test results:", savedResults);
|
|
170
|
-
// The real API doesn't always echo `testCase` back on the updated
|
|
171
|
-
// so fall back to
|
|
172
|
-
const
|
|
251
|
+
// The real API doesn't always echo `testCase`/`configuration` back on the updated
|
|
252
|
+
// results, so fall back to what we already know from step 4.
|
|
253
|
+
const resultInfoById = new Map();
|
|
173
254
|
for (const result of updatedResults) {
|
|
174
|
-
if (result.id
|
|
175
|
-
|
|
255
|
+
if (result.id) {
|
|
256
|
+
resultInfoById.set(result.id, {
|
|
257
|
+
caseId: result.testCase?.id,
|
|
258
|
+
configurationId: result.configuration?.id,
|
|
259
|
+
});
|
|
176
260
|
}
|
|
177
261
|
}
|
|
178
262
|
// 6. Upload Attachments
|
|
179
263
|
for (const savedResult of savedResults) {
|
|
180
264
|
if (!savedResult.id)
|
|
181
265
|
continue;
|
|
182
|
-
const
|
|
266
|
+
const info = resultInfoById.get(savedResult.id);
|
|
267
|
+
const caseId = savedResult.testCase?.id ?? info?.caseId;
|
|
183
268
|
if (!caseId)
|
|
184
269
|
continue;
|
|
185
|
-
const
|
|
270
|
+
const configurationId = savedResult.configuration?.id ?? info?.configurationId;
|
|
271
|
+
const matchedLocalResult = results.find((r) => matchesTestCase({
|
|
272
|
+
testCase: { id: caseId },
|
|
273
|
+
configuration: configurationId
|
|
274
|
+
? { id: configurationId }
|
|
275
|
+
: undefined,
|
|
276
|
+
}, r));
|
|
186
277
|
if (matchedLocalResult?.attachments &&
|
|
187
278
|
matchedLocalResult.attachments.length > 0) {
|
|
188
279
|
for (const attachment of matchedLocalResult.attachments) {
|
|
@@ -200,7 +291,7 @@ class AzureDevOpsService {
|
|
|
200
291
|
attachmentType: attachmentModel.attachmentType,
|
|
201
292
|
base64Length: attachment.base64Content?.length ?? 0,
|
|
202
293
|
});
|
|
203
|
-
const savedAttachment = await testApi.createTestResultAttachment(attachmentModel, this.config.
|
|
294
|
+
const savedAttachment = await testApi.createTestResultAttachment(attachmentModel, this.config.projectId, runId, savedResult.id);
|
|
204
295
|
this.debug("Attachment uploaded:", savedAttachment);
|
|
205
296
|
}
|
|
206
297
|
}
|
|
@@ -220,7 +311,7 @@ class AzureDevOpsService {
|
|
|
220
311
|
if (!this.enabled || !runId)
|
|
221
312
|
return;
|
|
222
313
|
const testApi = await this.testApiPromise;
|
|
223
|
-
await testApi.updateTestRun({ state: "Completed" }, this.config.
|
|
314
|
+
await testApi.updateTestRun({ state: "Completed" }, this.config.projectId, runId);
|
|
224
315
|
if (runId === this.currentRunId) {
|
|
225
316
|
this.currentRunId = undefined;
|
|
226
317
|
}
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ class AzureDevOpsReporterService {
|
|
|
22
22
|
results = [];
|
|
23
23
|
ado;
|
|
24
24
|
constructor(options) {
|
|
25
|
+
(0, azureService_1.assertRequiredOptions)(options);
|
|
25
26
|
this.options = options;
|
|
26
27
|
}
|
|
27
28
|
async afterTest(test, _context, results, browserInstance) {
|
|
@@ -50,6 +51,7 @@ class AzureDevOpsReporterService {
|
|
|
50
51
|
errorMessage: results.error?.message,
|
|
51
52
|
durationInMs: test.duration,
|
|
52
53
|
attachments,
|
|
54
|
+
configurationId: this.options.configurationId,
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
57
|
async onComplete() {
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface AzureDevOpsOptions {
|
|
|
3
3
|
orgUrl: string;
|
|
4
4
|
token: string;
|
|
5
5
|
/** Project display name or its GUID; Azure DevOps accepts either. */
|
|
6
|
-
|
|
6
|
+
projectId: string;
|
|
7
7
|
planId: number;
|
|
8
8
|
suiteId: number;
|
|
9
9
|
runName?: string;
|
|
@@ -21,12 +21,16 @@ export interface PublishOptions {
|
|
|
21
21
|
runId?: number;
|
|
22
22
|
/** Reuse already-fetched test points instead of calling `getPoints` again. */
|
|
23
23
|
points?: TestPoint[];
|
|
24
|
+
/** Only publish to the test point/result of this Azure DevOps configuration id; others are left untouched. */
|
|
25
|
+
configurationId?: number;
|
|
24
26
|
/** Leave the run in progress so more results can be added later. */
|
|
25
27
|
keepRunOpen?: boolean;
|
|
26
28
|
}
|
|
27
29
|
export interface AzureDevOpsWdioOptions extends AzureDevOpsOptions {
|
|
28
30
|
/** Attach a browser screenshot to failed results. Defaults to true. */
|
|
29
31
|
screenshotOnFailure?: boolean;
|
|
32
|
+
/** Azure DevOps test configuration id (e.g. Android vs iOS) this worker's results belong to. Required when the same test case is configured for multiple configurations, otherwise results can bleed across configurations. */
|
|
33
|
+
configurationId?: number;
|
|
30
34
|
}
|
|
31
35
|
export interface TestAttachment {
|
|
32
36
|
fileName: string;
|
|
@@ -40,4 +44,6 @@ export interface TestResultItem {
|
|
|
40
44
|
errorMessage?: string;
|
|
41
45
|
durationInMs?: number;
|
|
42
46
|
attachments?: TestAttachment[];
|
|
47
|
+
/** Azure DevOps test configuration id; disambiguates test points/results that share a case id across configurations (e.g. Android vs iOS). */
|
|
48
|
+
configurationId?: number;
|
|
43
49
|
}
|
package/dist/wdioService.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import type { Frameworks, Services } from "@wdio/types" with {
|
|
2
|
+
"resolution-mode": "import"
|
|
3
|
+
};
|
|
1
4
|
import { AzureDevOpsWdioOptions } from "./types";
|
|
2
5
|
/** Shares the run id created in the launcher process with the worker processes. */
|
|
3
6
|
export declare const RUN_ID_ENV_VAR = "AZURE_DEVOPS_TEST_RUN_ID";
|
|
4
|
-
interface WdioTest {
|
|
5
|
-
title: string;
|
|
6
|
-
fullTitle?: string;
|
|
7
|
-
}
|
|
8
|
-
interface WdioTestResult {
|
|
9
|
-
passed: boolean;
|
|
10
|
-
duration?: number;
|
|
11
|
-
error?: Error;
|
|
12
|
-
}
|
|
13
7
|
interface CucumberPickleTag {
|
|
14
8
|
name: string;
|
|
15
9
|
}
|
|
@@ -20,34 +14,30 @@ interface CucumberWorld {
|
|
|
20
14
|
tags?: CucumberPickleTag[];
|
|
21
15
|
};
|
|
22
16
|
}
|
|
23
|
-
interface CucumberResult {
|
|
24
|
-
passed: boolean;
|
|
25
|
-
duration?: number;
|
|
26
|
-
error?: Error;
|
|
27
|
-
}
|
|
28
17
|
/**
|
|
29
18
|
* WebdriverIO service that creates a single Test Run in `onPrepare`, pushes every
|
|
30
19
|
* spec's results into that run, and completes it in `onComplete`.
|
|
31
20
|
*/
|
|
32
|
-
export
|
|
33
|
-
private
|
|
21
|
+
export default class AzureDevOpsWdioService implements Services.ServiceInstance {
|
|
22
|
+
private readonly _options;
|
|
34
23
|
private results;
|
|
35
24
|
private service?;
|
|
36
|
-
constructor(
|
|
25
|
+
constructor(_options: AzureDevOpsWdioOptions);
|
|
37
26
|
onPrepare(): Promise<void>;
|
|
38
27
|
onComplete(): Promise<void>;
|
|
39
28
|
private debug;
|
|
40
|
-
afterTest(test:
|
|
29
|
+
afterTest(test: Frameworks.Test, _context: unknown, results: Frameworks.TestResult): Promise<void>;
|
|
41
30
|
/** Cucumber hook for BDD feature files; reads the case id from a `@C123` tag or the scenario name. */
|
|
42
|
-
afterScenario(world: CucumberWorld, result:
|
|
31
|
+
afterScenario(world: CucumberWorld, result: Frameworks.PickleResult): Promise<void>;
|
|
32
|
+
/** Cucumber's `error` is typed as a string, but some frameworks still pass a raw `Error`. */
|
|
33
|
+
private stringifyError;
|
|
43
34
|
after(): Promise<void>;
|
|
44
35
|
/** Publishes everything collected so far into the shared run. */
|
|
45
36
|
publish(): Promise<void>;
|
|
46
37
|
private captureScreenshot;
|
|
47
38
|
private extractCucumberCaseId;
|
|
39
|
+
/** `0` is treated as "not set", so a fresh run gets created. */
|
|
48
40
|
private resolveRunId;
|
|
49
41
|
private getService;
|
|
50
42
|
}
|
|
51
|
-
|
|
52
|
-
export declare const launcher: typeof AzureDevOpsWdioService;
|
|
53
|
-
export default AzureDevOpsWdioService;
|
|
43
|
+
export { AzureDevOpsWdioService };
|
package/dist/wdioService.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.AzureDevOpsWdioService = exports.RUN_ID_ENV_VAR = void 0;
|
|
4
4
|
const azureService_1 = require("./azureService");
|
|
5
5
|
const utils_1 = require("./utils");
|
|
6
6
|
/** Shares the run id created in the launcher process with the worker processes. */
|
|
@@ -10,14 +10,21 @@ exports.RUN_ID_ENV_VAR = "AZURE_DEVOPS_TEST_RUN_ID";
|
|
|
10
10
|
* spec's results into that run, and completes it in `onComplete`.
|
|
11
11
|
*/
|
|
12
12
|
class AzureDevOpsWdioService {
|
|
13
|
-
|
|
13
|
+
_options;
|
|
14
14
|
results = [];
|
|
15
15
|
service;
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
16
|
+
constructor(_options) {
|
|
17
|
+
this._options = _options;
|
|
18
|
+
(0, azureService_1.assertRequiredOptions)(_options);
|
|
18
19
|
}
|
|
19
20
|
// --- launcher process hooks ---
|
|
20
21
|
async onPrepare() {
|
|
22
|
+
const existingRunId = this.resolveRunId();
|
|
23
|
+
if (existingRunId) {
|
|
24
|
+
process.env[exports.RUN_ID_ENV_VAR] = existingRunId.toString();
|
|
25
|
+
console.log(`Reusing Azure DevOps test run: ${existingRunId}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
21
28
|
const runId = await this.getService().createRun();
|
|
22
29
|
if (runId === undefined)
|
|
23
30
|
return;
|
|
@@ -34,15 +41,15 @@ class AzureDevOpsWdioService {
|
|
|
34
41
|
}
|
|
35
42
|
// --- worker process hooks ---
|
|
36
43
|
debug(message, payload) {
|
|
37
|
-
if (!this.
|
|
44
|
+
if (!this._options.debug)
|
|
38
45
|
return;
|
|
39
46
|
console.log(message, payload);
|
|
40
47
|
}
|
|
41
48
|
async afterTest(test, _context, results) {
|
|
42
|
-
const caseId = (0, utils_1.extractTestCaseId)(test.title, this.
|
|
49
|
+
const caseId = (0, utils_1.extractTestCaseId)(test.title, this._options.caseIdPattern);
|
|
43
50
|
this.debug("Extracted case id from test title:", {
|
|
44
51
|
title: test.title,
|
|
45
|
-
pattern: String(this.
|
|
52
|
+
pattern: String(this._options.caseIdPattern ?? "default C123/#123"),
|
|
46
53
|
caseId,
|
|
47
54
|
});
|
|
48
55
|
if (!caseId)
|
|
@@ -53,6 +60,7 @@ class AzureDevOpsWdioService {
|
|
|
53
60
|
errorMessage: results.error?.message,
|
|
54
61
|
durationInMs: results.duration ?? 0,
|
|
55
62
|
attachments: await this.captureScreenshot(caseId, test, results),
|
|
63
|
+
configurationId: this._options.configurationId,
|
|
56
64
|
});
|
|
57
65
|
}
|
|
58
66
|
/** Cucumber hook for BDD feature files; reads the case id from a `@C123` tag or the scenario name. */
|
|
@@ -63,11 +71,18 @@ class AzureDevOpsWdioService {
|
|
|
63
71
|
this.results.push({
|
|
64
72
|
testCaseId: caseId,
|
|
65
73
|
outcome: result.passed ? "Passed" : "Failed",
|
|
66
|
-
errorMessage: result.error
|
|
74
|
+
errorMessage: this.stringifyError(result.error),
|
|
67
75
|
durationInMs: result.duration ?? 0,
|
|
68
76
|
attachments: await this.captureScreenshot(caseId, { title: world.pickle.name }, result),
|
|
77
|
+
configurationId: this._options.configurationId,
|
|
69
78
|
});
|
|
70
79
|
}
|
|
80
|
+
/** Cucumber's `error` is typed as a string, but some frameworks still pass a raw `Error`. */
|
|
81
|
+
stringifyError(error) {
|
|
82
|
+
if (!error)
|
|
83
|
+
return undefined;
|
|
84
|
+
return error instanceof Error ? error.message : String(error);
|
|
85
|
+
}
|
|
71
86
|
async after() {
|
|
72
87
|
await this.publish();
|
|
73
88
|
}
|
|
@@ -81,6 +96,7 @@ class AzureDevOpsWdioService {
|
|
|
81
96
|
await this.getService().publishResults(pending, {
|
|
82
97
|
runId: this.resolveRunId(),
|
|
83
98
|
keepRunOpen: true,
|
|
99
|
+
configurationId: this._options.configurationId,
|
|
84
100
|
});
|
|
85
101
|
}
|
|
86
102
|
catch (err) {
|
|
@@ -88,7 +104,7 @@ class AzureDevOpsWdioService {
|
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
async captureScreenshot(caseId, test, results) {
|
|
91
|
-
if (results.passed || this.
|
|
107
|
+
if (results.passed || this._options.screenshotOnFailure === false)
|
|
92
108
|
return [];
|
|
93
109
|
const browser = globalThis
|
|
94
110
|
.browser;
|
|
@@ -109,7 +125,7 @@ class AzureDevOpsWdioService {
|
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
extractCucumberCaseId(world) {
|
|
112
|
-
const pattern = this.
|
|
128
|
+
const pattern = this._options.caseIdPattern;
|
|
113
129
|
const tags = world.pickle.tags ?? [];
|
|
114
130
|
this.debug("Scanning scenario tags for case id:", {
|
|
115
131
|
scenario: world.pickle.name,
|
|
@@ -130,17 +146,17 @@ class AzureDevOpsWdioService {
|
|
|
130
146
|
});
|
|
131
147
|
return fromName;
|
|
132
148
|
}
|
|
149
|
+
/** `0` is treated as "not set", so a fresh run gets created. */
|
|
133
150
|
resolveRunId() {
|
|
134
151
|
const fromEnv = process.env[exports.RUN_ID_ENV_VAR];
|
|
135
152
|
const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;
|
|
136
|
-
|
|
153
|
+
const runId = Number.isNaN(parsed) ? this._options.runId : parsed;
|
|
154
|
+
return runId || undefined;
|
|
137
155
|
}
|
|
138
156
|
getService() {
|
|
139
|
-
this.service ??= new azureService_1.AzureDevOpsService(this.
|
|
157
|
+
this.service ??= new azureService_1.AzureDevOpsService(this._options);
|
|
140
158
|
return this.service;
|
|
141
159
|
}
|
|
142
160
|
}
|
|
143
|
-
exports.AzureDevOpsWdioService = AzureDevOpsWdioService;
|
|
144
|
-
/** WebdriverIO looks for a `launcher` export to run `onPrepare`/`onComplete`. */
|
|
145
|
-
exports.launcher = AzureDevOpsWdioService;
|
|
146
161
|
exports.default = AzureDevOpsWdioService;
|
|
162
|
+
exports.AzureDevOpsWdioService = AzureDevOpsWdioService;
|
package/package.json
CHANGED
|
@@ -1,62 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@virag8/azure-devops-test-publisher",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Upload test results and screenshots to Azure DevOps Test Plans from WebdriverIO or custom TS runners",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"author": "virag <viragkumar58@gmail.com>",
|
|
9
|
-
"homepage": "https://github.com/viragkumar/azure-devops-test-publisher#readme",
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/viragkumar/azure-devops-test-publisher.git"
|
|
13
|
-
},
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/viragkumar/azure-devops-test-publisher/issues"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"webdriverio",
|
|
19
|
-
"wdio",
|
|
20
|
-
"wdio-service",
|
|
21
|
-
"ado",
|
|
22
|
-
"azure-devops",
|
|
23
|
-
"azure-test-plans",
|
|
24
|
-
"test-results",
|
|
25
|
-
"reporter",
|
|
26
|
-
"cucumber",
|
|
27
|
-
"mocha",
|
|
28
|
-
"typescript",
|
|
29
|
-
"azure-devops-test-publisher",
|
|
30
|
-
"test-publisher"
|
|
31
|
-
],
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=18"
|
|
34
|
-
},
|
|
35
|
-
"files": [
|
|
36
|
-
"dist",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE"
|
|
39
|
-
],
|
|
40
|
-
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "rimraf dist && tsc",
|
|
45
|
-
"prepublishOnly": "npm run test:unit && npm run build",
|
|
46
|
-
"test:unit": "jest --config tests/jest.config.js",
|
|
47
|
-
"test:real": "jest --config tests/jest.real.config.js",
|
|
48
|
-
"test": "npm run test:unit"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"@types/
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@virag8/azure-devops-test-publisher",
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "Upload test results and screenshots to Azure DevOps Test Plans from WebdriverIO or custom TS runners",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "virag <viragkumar58@gmail.com>",
|
|
9
|
+
"homepage": "https://github.com/viragkumar/azure-devops-test-publisher#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/viragkumar/azure-devops-test-publisher.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/viragkumar/azure-devops-test-publisher/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"webdriverio",
|
|
19
|
+
"wdio",
|
|
20
|
+
"wdio-service",
|
|
21
|
+
"ado",
|
|
22
|
+
"azure-devops",
|
|
23
|
+
"azure-test-plans",
|
|
24
|
+
"test-results",
|
|
25
|
+
"reporter",
|
|
26
|
+
"cucumber",
|
|
27
|
+
"mocha",
|
|
28
|
+
"typescript",
|
|
29
|
+
"azure-devops-test-publisher",
|
|
30
|
+
"test-publisher"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "rimraf dist && tsc",
|
|
45
|
+
"prepublishOnly": "npm run test:unit && npm run build",
|
|
46
|
+
"test:unit": "jest --config tests/jest.config.js",
|
|
47
|
+
"test:real": "jest --config tests/jest.real.config.js",
|
|
48
|
+
"test": "npm run test:unit",
|
|
49
|
+
"pack:local": "npm run build && npm pack"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"azure-devops-node-api": "^12.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/jest": "^30.0.0",
|
|
56
|
+
"@types/node": "^20.0.0",
|
|
57
|
+
"@wdio/types": "^9.31.2",
|
|
58
|
+
"dotenv": "^17.4.2",
|
|
59
|
+
"jest": "^30.5.1",
|
|
60
|
+
"rimraf": "^5.0.0",
|
|
61
|
+
"ts-jest": "^29.4.12",
|
|
62
|
+
"typescript": "^5.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|