@allurereport/plugin-testops 3.3.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,13 +51,14 @@ export default defineConfig({
51
51
 
52
52
  The plugin accepts the following options:
53
53
 
54
- | Option | Description | Type | Default |
55
- |------------------|-------------------------------------------------|---------|-------------------------|
56
- | `launchName` | Name of the report which will be assigned to the new launch | `string` | `Allure Report` |
57
- | `launchTags` | Tags to be assigned to the new launch | `string[]` | `[]` |
58
- | `accessToken` | Access token for TestOps API | `string` | `undefined` |
59
- | `endpoint` | TestOps API endpoint | `string` | `undefined` |
60
- | `projectId` | TestOps project ID | `string` | `undefined` |
54
+ | Option | Description | Type | Default |
55
+ |--------------------|-----------------------------------------------------------------------------|-----------|-----------------|
56
+ | `launchName` | Name of the report which will be assigned to the new launch | `string` | `Allure Report` |
57
+ | `launchTags` | Tags to be assigned to the new launch | `string[]`| `[]` |
58
+ | `accessToken` | Access token for TestOps API | `string` | `undefined` |
59
+ | `endpoint` | TestOps API endpoint | `string` | `undefined` |
60
+ | `projectId` | TestOps project ID | `string` | `undefined` |
61
+ | `autocloseLaunch` | When `true` (default), the launch is closed automatically when the plugin finishes; set to `false` to keep the launch open | `boolean` | `true` |
61
62
 
62
63
  ### Using options from environment variables
63
64
 
package/dist/client.d.ts CHANGED
@@ -1,22 +1,49 @@
1
- import type { CiDescriptor, TestResult, TestStatus } from "@allurereport/core-api";
1
+ import type { ClientRequest } from "http";
2
+ import type { AttachmentLink, CiDescriptor, EnvironmentIdentity, TestError, TestResult, TestStatus } from "@allurereport/core-api";
3
+ import type { QualityGateValidationResult } from "@allurereport/plugin-api";
4
+ import { AxiosError, type AxiosResponse } from "axios";
5
+ import type { AttachmentForUpload, AttachmentsResolver, FixtureResolver, LaunchCategoryBulkItem, LaunchCategoryBulkResult, TestOpsClientParams, TestOpsNamedEnv, TestOpsPluginTestResult } from "./model.js";
6
+ declare class TestOpsClientError extends AxiosError<{
7
+ message: string;
8
+ timestamp: number;
9
+ status: number;
10
+ }> {
11
+ response: AxiosResponse<{
12
+ message: string;
13
+ timestamp: number;
14
+ status: number;
15
+ }>;
16
+ request: ClientRequest;
17
+ }
2
18
  export declare class TestOpsClient {
3
19
  #private;
4
- constructor(params: {
5
- baseUrl: string;
6
- projectId: string;
7
- accessToken: string;
8
- limit?: number;
9
- });
20
+ constructor(params: TestOpsClientParams);
21
+ isTestOpsClientError(error: unknown): error is TestOpsClientError;
10
22
  get launchUrl(): string | undefined;
23
+ get launchId(): number | undefined;
24
+ closeLaunch(launchId: number): Promise<void>;
25
+ createLaunchCategoriesBulk(launchId: number, items: LaunchCategoryBulkItem[]): Promise<LaunchCategoryBulkResult[]>;
11
26
  issueOauthToken(): Promise<void>;
12
27
  startUpload(ci: CiDescriptor): Promise<void>;
13
28
  stopUpload(ci: CiDescriptor, status: TestStatus): Promise<void>;
14
29
  createLaunch(launchName: string, launchTags: string[]): Promise<void>;
15
30
  createSession(environment?: Record<string, any>): Promise<void>;
31
+ get namedEnvs(): MapIterator<TestOpsNamedEnv>;
32
+ getNamedEnvFor(id: string): TestOpsNamedEnv | undefined;
33
+ createNamedEnvs(environments: EnvironmentIdentity[], onProgress?: (percent: number, total: number) => void): Promise<void>;
34
+ uploadGlobalAttachments(params: {
35
+ attachments: AttachmentLink[];
36
+ attachmentsResolver: (attachment: AttachmentLink) => Promise<AttachmentForUpload | undefined>;
37
+ onProgress?: (percent: number, total: number) => void;
38
+ }): Promise<void>;
39
+ uploadGlobalErrors(errors: TestError[], onProgress?: (percent: number, total: number) => void): Promise<void>;
16
40
  uploadTestResults(params: {
17
- trs: TestResult[];
18
- attachmentsResolver: (tr: TestResult) => Promise<any>;
19
- fixturesResolver: (tr: TestResult) => Promise<any>;
41
+ trs: TestOpsPluginTestResult[];
42
+ environments: EnvironmentIdentity[];
43
+ attachmentsResolver: AttachmentsResolver;
44
+ fixturesResolver: FixtureResolver;
20
45
  onProgress?: () => void;
21
- }): Promise<void>;
46
+ }): Promise<TestResult[]>;
47
+ uploadQualityGateResults(results: QualityGateValidationResult[], onProgress?: (percent: number, total: number) => void): Promise<void>;
22
48
  }
49
+ export {};
package/dist/client.js CHANGED
@@ -9,13 +9,21 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _TestOpsClient_accessToken, _TestOpsClient_projectId, _TestOpsClient_oauthToken, _TestOpsClient_client, _TestOpsClient_launch, _TestOpsClient_session, _TestOpsClient_uploadInProgress, _TestOpsClient_uploadLimit;
13
- import axios from "axios";
12
+ var _TestOpsClient_instances, _TestOpsClient_logger, _TestOpsClient_accessToken, _TestOpsClient_projectId, _TestOpsClient_oauthToken, _TestOpsClient_client, _TestOpsClient_launch, _TestOpsClient_session, _TestOpsClient_uploadInProgress, _TestOpsClient_uploadLimit, _TestOpsClient_namedEnvsIdsByEnv, _TestOpsClient_postTestResultsChunk, _TestOpsClient_uploadChunkAttachmentsAndFixtures, _TestOpsClient_uploadAttachmentsForResult, _TestOpsClient_uploadFixturesForResult;
13
+ import axios, { AxiosError, isAxiosError } from "axios";
14
14
  import FormData from "form-data";
15
- import chunk from "lodash.chunk";
15
+ import { chunk } from "lodash-es";
16
16
  import pLimit from "p-limit";
17
+ import { bold } from "yoctocolors";
18
+ import { Logger } from "./logger.js";
19
+ class TestOpsClientError extends AxiosError {
20
+ }
21
+ const CHUNK_SIZE = 100;
22
+ const BULK_UPLOAD_CHUNK_SIZE = 1000;
17
23
  export class TestOpsClient {
18
24
  constructor(params) {
25
+ _TestOpsClient_instances.add(this);
26
+ _TestOpsClient_logger.set(this, new Logger("TestOpsClient"));
19
27
  _TestOpsClient_accessToken.set(this, void 0);
20
28
  _TestOpsClient_projectId.set(this, void 0);
21
29
  _TestOpsClient_oauthToken.set(this, "");
@@ -24,6 +32,7 @@ export class TestOpsClient {
24
32
  _TestOpsClient_session.set(this, void 0);
25
33
  _TestOpsClient_uploadInProgress.set(this, false);
26
34
  _TestOpsClient_uploadLimit.set(this, 1);
35
+ _TestOpsClient_namedEnvsIdsByEnv.set(this, new Map());
27
36
  if (!params.accessToken) {
28
37
  throw new Error("accessToken is required");
29
38
  }
@@ -42,28 +51,86 @@ export class TestOpsClient {
42
51
  baseURL: params.baseUrl,
43
52
  validateStatus: (status) => status >= 200 && status < 400,
44
53
  }), "f");
54
+ __classPrivateFieldGet(this, _TestOpsClient_client, "f").interceptors.request.use((config) => {
55
+ if (__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")) {
56
+ config.headers.set("Authorization", `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`);
57
+ }
58
+ return config;
59
+ });
45
60
  if (params.limit) {
46
61
  __classPrivateFieldSet(this, _TestOpsClient_uploadLimit, params.limit, "f");
47
62
  }
48
63
  }
64
+ isTestOpsClientError(error) {
65
+ return (isAxiosError(error) &&
66
+ typeof error.response?.data.status === "number" &&
67
+ typeof error.response?.data.message === "string");
68
+ }
49
69
  get launchUrl() {
50
70
  if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
51
71
  return undefined;
52
72
  }
53
73
  return new URL(`launch/${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id}`, __classPrivateFieldGet(this, _TestOpsClient_client, "f").defaults.baseURL).toString();
54
74
  }
75
+ get launchId() {
76
+ return __classPrivateFieldGet(this, _TestOpsClient_launch, "f")?.id;
77
+ }
78
+ async closeLaunch(launchId) {
79
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Closing launch…");
80
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/launch/${launchId}/close`);
81
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Launch closed");
82
+ }
83
+ async createLaunchCategoriesBulk(launchId, items) {
84
+ if (items.length === 0) {
85
+ return [];
86
+ }
87
+ const results = [];
88
+ const uploadChunk = async (chunk, currentRequestIndex, totalChunks) => {
89
+ const body = { launchId, items: chunk };
90
+ if (totalChunks === 1) {
91
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(`POST /api/launch/category/bulk request (items: ${chunk.length})`);
92
+ }
93
+ else {
94
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(`POST /api/launch/category/bulk request (${currentRequestIndex + 1}/${totalChunks}, items: ${chunk.length})`);
95
+ }
96
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(body);
97
+ const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/category/bulk", body, {
98
+ headers: { "Content-Type": "application/json" },
99
+ });
100
+ if (Array.isArray(data)) {
101
+ results.push(...data);
102
+ }
103
+ };
104
+ if (items.length <= BULK_UPLOAD_CHUNK_SIZE) {
105
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`Creating ${bold(items.length.toString())} launch categories…`);
106
+ await uploadChunk(items, 0, 1);
107
+ }
108
+ else {
109
+ const chunks = chunk(items, BULK_UPLOAD_CHUNK_SIZE);
110
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`Creating ${bold(items.length.toString())} launch categories in ${bold(chunks.length.toString())} request(s)…`);
111
+ for (let i = 0; i < chunks.length; i += 1) {
112
+ await uploadChunk(chunks[i], i, chunks.length);
113
+ }
114
+ }
115
+ return results;
116
+ }
55
117
  async issueOauthToken() {
118
+ const base = __classPrivateFieldGet(this, _TestOpsClient_client, "f").defaults.baseURL;
119
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(`Endpoint: ${base}`);
120
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Issuing OAuth token…");
56
121
  const formData = new FormData();
57
122
  formData.append("grant_type", "apitoken");
58
123
  formData.append("scope", "openid");
59
124
  formData.append("token", __classPrivateFieldGet(this, _TestOpsClient_accessToken, "f"));
60
125
  const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/uaa/oauth/token", formData);
61
126
  __classPrivateFieldSet(this, _TestOpsClient_oauthToken, data.access_token, "f");
127
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("OAuth token received");
62
128
  }
63
129
  async startUpload(ci) {
64
130
  if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
65
131
  throw new Error("Launch isn't created! Call createLaunch first");
66
132
  }
133
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`Starting CI upload (${ci.type})…`);
67
134
  await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/start", {
68
135
  projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
69
136
  ci: {
@@ -79,12 +146,9 @@ export class TestOpsClient {
79
146
  launch: {
80
147
  id: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
81
148
  },
82
- }, {
83
- headers: {
84
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
85
- },
86
149
  });
87
150
  __classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, true, "f");
151
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("CI upload started");
88
152
  }
89
153
  async stopUpload(ci, status) {
90
154
  if (!__classPrivateFieldGet(this, _TestOpsClient_uploadInProgress, "f")) {
@@ -95,96 +159,283 @@ export class TestOpsClient {
95
159
  jobUid: ci.jobUid,
96
160
  projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
97
161
  status,
98
- }, {
99
- headers: {
100
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
101
- },
102
162
  });
103
163
  __classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, false, "f");
164
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`CI upload stopped (status: ${status})`);
104
165
  }
105
166
  async createLaunch(launchName, launchTags) {
167
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Creating launch…");
106
168
  const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch", {
107
169
  name: launchName,
108
170
  projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
109
171
  autoclose: true,
110
172
  external: true,
111
173
  tags: launchTags.map((tag) => ({ name: tag })),
112
- }, {
113
- headers: {
114
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
115
- },
116
174
  });
117
175
  __classPrivateFieldSet(this, _TestOpsClient_launch, data, "f");
176
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(`Launch created: id=${bold(data.id.toString())}`);
118
177
  }
119
178
  async createSession(environment = {}) {
120
179
  if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
121
180
  throw new Error("Launch isn't created! Call createLaunch first");
122
181
  }
123
- const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/session?manual=true", {
182
+ const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/session", {
124
183
  launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
125
- environment: Object.entries(environment).map(([key, value]) => ({ key, value: String(value) })),
184
+ environment: Object.entries(environment).map(([key, value]) => ({
185
+ key,
186
+ value: String(value),
187
+ })),
126
188
  }, {
127
- headers: {
128
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
189
+ params: {
190
+ manual: "true",
129
191
  },
130
192
  });
131
193
  __classPrivateFieldSet(this, _TestOpsClient_session, data, "f");
132
194
  }
195
+ get namedEnvs() {
196
+ return __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").values();
197
+ }
198
+ getNamedEnvFor(id) {
199
+ for (const [, env] of __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f")) {
200
+ if (env.externalId === id) {
201
+ return env;
202
+ }
203
+ }
204
+ return undefined;
205
+ }
206
+ async createNamedEnvs(environments, onProgress) {
207
+ if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
208
+ throw new Error("Session isn't created! Call createSession first");
209
+ }
210
+ if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
211
+ throw new Error("Launch isn't created! Call createLaunch first");
212
+ }
213
+ const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/named-env/bulk", {
214
+ launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
215
+ items: environments.map(({ id, name }) => ({
216
+ externalId: id,
217
+ name,
218
+ })),
219
+ }, {
220
+ headers: {
221
+ "Content-Type": "application/json",
222
+ },
223
+ onUploadProgress(progressEvent) {
224
+ const total = progressEvent.total ?? 100;
225
+ const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
226
+ onProgress?.(percent, total);
227
+ },
228
+ });
229
+ const namesById = new Map(environments.map(({ id, name }) => [id, name]));
230
+ data.forEach((env) => {
231
+ __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").set(env.externalId, {
232
+ ...env,
233
+ name: namesById.get(env.externalId) ?? env.externalId,
234
+ });
235
+ });
236
+ }
237
+ async uploadGlobalAttachments(params) {
238
+ const { attachments, attachmentsResolver, onProgress } = params;
239
+ if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
240
+ throw new Error("Session isn't created! Call createSession first");
241
+ }
242
+ if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
243
+ throw new Error("Launch isn't created! Call createLaunch first");
244
+ }
245
+ const formData = new FormData();
246
+ for (const attachmentLink of attachments) {
247
+ const attachment = await attachmentsResolver(attachmentLink);
248
+ if (!attachment) {
249
+ continue;
250
+ }
251
+ formData.append("file", attachment.content, {
252
+ filename: attachment.originalFileName,
253
+ contentType: attachment.contentType,
254
+ });
255
+ }
256
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/attachment", formData, {
257
+ onUploadProgress(progressEvent) {
258
+ const total = progressEvent.total ?? 100;
259
+ const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
260
+ onProgress?.(percent, total);
261
+ },
262
+ params: { launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id },
263
+ headers: formData.getHeaders(),
264
+ });
265
+ }
266
+ async uploadGlobalErrors(errors, onProgress) {
267
+ if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
268
+ throw new Error("Session isn't created! Call createSession first");
269
+ }
270
+ if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
271
+ throw new Error("Launch isn't created! Call createLaunch first");
272
+ }
273
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/error/bulk", {
274
+ launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
275
+ items: errors,
276
+ }, {
277
+ onUploadProgress(progressEvent) {
278
+ const total = progressEvent.total ?? 100;
279
+ const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
280
+ onProgress?.(percent, total);
281
+ },
282
+ });
283
+ }
133
284
  async uploadTestResults(params) {
134
285
  if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
135
286
  throw new Error("Session isn't created! Call createSession first");
136
287
  }
137
- const { trs, attachmentsResolver, fixturesResolver, onProgress } = params;
138
- const trsChunks = chunk(trs, 100);
288
+ const { trs, environments, attachmentsResolver, fixturesResolver, onProgress } = params;
289
+ const trsChunks = chunk(trs, CHUNK_SIZE);
139
290
  const uploadLimitFn = pLimit(__classPrivateFieldGet(this, _TestOpsClient_uploadLimit, "f"));
140
- await Promise.all(trsChunks.map(async (trsChunk) => {
141
- const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/test-result", {
142
- testSessionId: __classPrivateFieldGet(this, _TestOpsClient_session, "f").id,
143
- results: trsChunk.map((tr) => ({
144
- ...tr,
145
- uuid: tr.id,
146
- })),
147
- }, {
148
- headers: {
149
- "Authorization": `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
150
- "Content-Type": "application/json",
151
- },
152
- });
153
- const trsTestOpsIdsByUuid = data.results.reduce((acc, { id, uuid }) => ({ ...acc, [uuid]: id }), {});
154
- await Promise.all(trsChunk.map((tr) => uploadLimitFn(async () => {
155
- const trTestOpsId = trsTestOpsIdsByUuid[tr.id];
156
- const attachments = await attachmentsResolver(tr);
157
- const fixtures = await fixturesResolver(tr);
158
- if (attachments.length > 0) {
159
- const attachmentsChunks = chunk(attachments, 100);
160
- await Promise.all(attachmentsChunks.map(async (attachmentsChunk) => {
161
- const formData = new FormData();
162
- attachmentsChunk.forEach((attachment) => {
163
- formData.append("file", attachment.content, {
164
- filename: attachment.originalFileName,
165
- contentType: attachment.contentType,
166
- });
167
- });
168
- await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${trTestOpsId}/attachment`, formData, {
169
- headers: {
170
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
171
- ...formData.getHeaders(),
172
- },
291
+ const uploadedTrs = [];
292
+ const envNamesById = new Map(environments.map(({ id, name }) => [id, name]));
293
+ try {
294
+ for (const trsChunk of trsChunks) {
295
+ const chunkEnvs = new Map();
296
+ for (const tr of trsChunk) {
297
+ const environmentId = tr.environment;
298
+ if (environmentId && !__classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").has(environmentId)) {
299
+ chunkEnvs.set(environmentId, {
300
+ id: environmentId,
301
+ name: envNamesById.get(environmentId) ?? environmentId,
173
302
  });
174
- }));
303
+ }
175
304
  }
176
- if (fixtures.length > 0) {
177
- await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${trTestOpsId}/test-fixture-result`, {
178
- fixtures,
179
- }, {
180
- headers: {
181
- Authorization: `Bearer ${__classPrivateFieldGet(this, _TestOpsClient_oauthToken, "f")}`,
182
- },
183
- });
305
+ if (chunkEnvs.size > 0) {
306
+ await this.createNamedEnvs(Array.from(chunkEnvs.values()));
184
307
  }
185
- onProgress?.();
186
- })));
187
- }));
308
+ const reportIdsToTestOpsIds = await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_postTestResultsChunk).call(this, trsChunk);
309
+ await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadChunkAttachmentsAndFixtures).call(this, trsChunk, reportIdsToTestOpsIds, attachmentsResolver, fixturesResolver, uploadLimitFn, onProgress);
310
+ uploadedTrs.push(...trsChunk);
311
+ }
312
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Test results upload completed");
313
+ }
314
+ catch (error) {
315
+ if (this.isTestOpsClientError(error)) {
316
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload test results: ${error.response?.data.message}`);
317
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(error.response.data);
318
+ }
319
+ else if (error instanceof Error) {
320
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload test results: ${error.message}`);
321
+ }
322
+ else {
323
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error("Failed to upload test results");
324
+ }
325
+ }
326
+ return uploadedTrs;
327
+ }
328
+ async uploadQualityGateResults(results, onProgress) {
329
+ if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
330
+ throw new Error("Session isn't created! Call createSession first");
331
+ }
332
+ if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
333
+ throw new Error("Launch isn't created! Call createLaunch first");
334
+ }
335
+ const items = results.map((result) => {
336
+ const item = {
337
+ name: result.rule,
338
+ message: result.message,
339
+ };
340
+ const namedEnvId = !!result.environment && this.getNamedEnvFor(result.environment)?.id;
341
+ if (typeof namedEnvId === "number") {
342
+ item.namedEnvId = namedEnvId;
343
+ }
344
+ return item;
345
+ });
346
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/quality-gate/bulk", {
347
+ launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
348
+ items,
349
+ }, {
350
+ onUploadProgress(progressEvent) {
351
+ const total = progressEvent.total ?? 100;
352
+ const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
353
+ onProgress?.(percent, total);
354
+ },
355
+ });
188
356
  }
189
357
  }
190
- _TestOpsClient_accessToken = new WeakMap(), _TestOpsClient_projectId = new WeakMap(), _TestOpsClient_oauthToken = new WeakMap(), _TestOpsClient_client = new WeakMap(), _TestOpsClient_launch = new WeakMap(), _TestOpsClient_session = new WeakMap(), _TestOpsClient_uploadInProgress = new WeakMap(), _TestOpsClient_uploadLimit = new WeakMap();
358
+ _TestOpsClient_logger = new WeakMap(), _TestOpsClient_accessToken = new WeakMap(), _TestOpsClient_projectId = new WeakMap(), _TestOpsClient_oauthToken = new WeakMap(), _TestOpsClient_client = new WeakMap(), _TestOpsClient_launch = new WeakMap(), _TestOpsClient_session = new WeakMap(), _TestOpsClient_uploadInProgress = new WeakMap(), _TestOpsClient_uploadLimit = new WeakMap(), _TestOpsClient_namedEnvsIdsByEnv = new WeakMap(), _TestOpsClient_instances = new WeakSet(), _TestOpsClient_postTestResultsChunk = async function _TestOpsClient_postTestResultsChunk(trsChunk) {
359
+ const { data } = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/test-result", {
360
+ testSessionId: __classPrivateFieldGet(this, _TestOpsClient_session, "f").id,
361
+ results: trsChunk.map((testResult) => {
362
+ const extendedTestResult = {
363
+ ...testResult,
364
+ uuid: testResult.id,
365
+ };
366
+ const namedEnvironment = !!testResult.environment && this.getNamedEnvFor(testResult.environment);
367
+ if (namedEnvironment) {
368
+ extendedTestResult.namedEnv = { id: namedEnvironment.id };
369
+ }
370
+ if (typeof extendedTestResult.category?.externalId === "string" ||
371
+ typeof extendedTestResult.category?.externalId === "number") {
372
+ const category = extendedTestResult.category;
373
+ extendedTestResult.category = {
374
+ externalId: category.externalId,
375
+ };
376
+ if (category.grouping && category.grouping.length > 0) {
377
+ extendedTestResult.category.grouping = category.grouping;
378
+ }
379
+ }
380
+ const error = extendedTestResult.error;
381
+ if (typeof error?.message === "string") {
382
+ extendedTestResult.message = error.message;
383
+ }
384
+ if (typeof error?.trace === "string") {
385
+ extendedTestResult.trace = error.trace;
386
+ }
387
+ return extendedTestResult;
388
+ }),
389
+ }, { headers: { "Content-Type": "application/json" } });
390
+ const reportIdsToTestOpsIds = {};
391
+ for (const { id, uuid } of data.results ?? []) {
392
+ reportIdsToTestOpsIds[uuid] = id;
393
+ }
394
+ return reportIdsToTestOpsIds;
395
+ }, _TestOpsClient_uploadChunkAttachmentsAndFixtures = async function _TestOpsClient_uploadChunkAttachmentsAndFixtures(trsChunk, reportIdsToTestOpsIds, attachmentsResolver, fixturesResolver, uploadLimitFn, onProgress) {
396
+ await Promise.all(trsChunk.map((tr) => uploadLimitFn(async () => {
397
+ const testOpsId = reportIdsToTestOpsIds[tr.id];
398
+ const attachments = await attachmentsResolver(tr);
399
+ const fixtures = await fixturesResolver(tr);
400
+ await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadAttachmentsForResult).call(this, testOpsId, attachments);
401
+ await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadFixturesForResult).call(this, testOpsId, fixtures);
402
+ onProgress?.();
403
+ })));
404
+ }, _TestOpsClient_uploadAttachmentsForResult = async function _TestOpsClient_uploadAttachmentsForResult(testOpsResultId, attachments) {
405
+ if (attachments.length === 0) {
406
+ return;
407
+ }
408
+ const attachmentsChunks = chunk(attachments, 100);
409
+ for (const attachmentsChunk of attachmentsChunks) {
410
+ const formData = new FormData();
411
+ for (const att of attachmentsChunk) {
412
+ formData.append("file", att.content, {
413
+ filename: att.originalFileName,
414
+ contentType: att.contentType,
415
+ });
416
+ }
417
+ try {
418
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${testOpsResultId}/attachment`, formData, {
419
+ headers: formData.getHeaders(),
420
+ });
421
+ }
422
+ catch (error) {
423
+ if (this.isTestOpsClientError(error)) {
424
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}: ${error.response?.data.message}`);
425
+ }
426
+ else if (error instanceof Error) {
427
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}: ${error.message}`);
428
+ }
429
+ else {
430
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}`);
431
+ }
432
+ __classPrivateFieldGet(this, _TestOpsClient_logger, "f").inspect(formData);
433
+ }
434
+ }
435
+ }, _TestOpsClient_uploadFixturesForResult = async function _TestOpsClient_uploadFixturesForResult(testOpsResultId, fixtures) {
436
+ if (fixtures.length === 0)
437
+ return;
438
+ await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${testOpsResultId}/test-fixture-result`, {
439
+ fixtures,
440
+ });
441
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { TestopsPluginOptions as TestopsUploaderPluginOptions } from "./model.js";
2
- export { TestopsPlugin as default } from "./plugin.js";
1
+ export type { TestOpsPluginOptions as TestOpsUploaderPluginOptions } from "./model.js";
2
+ export { TestOpsPlugin as default } from "./plugin.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { TestopsPlugin as default } from "./plugin.js";
1
+ export { TestOpsPlugin as default } from "./plugin.js";
@@ -0,0 +1,24 @@
1
+ import ProgressBar from "progress";
2
+ type JSONMessage = Record<string, unknown> | Array<unknown>;
3
+ export declare class Logger {
4
+ #private;
5
+ readonly name: string;
6
+ constructor(name: string);
7
+ progressBarCounter(message: string, total: number): ProgressBar | {
8
+ tick: () => void;
9
+ update: () => void;
10
+ terminate: () => void;
11
+ };
12
+ progressBar(message: string): ProgressBar | {
13
+ update: () => void;
14
+ terminate: () => void;
15
+ };
16
+ verbose(message: string | JSONMessage): void;
17
+ debug(message: string | JSONMessage): void;
18
+ newLine(): void;
19
+ info(message: string | JSONMessage): void;
20
+ warn(message: string | JSONMessage): void;
21
+ error(message: string | JSONMessage): void;
22
+ inspect(value: unknown): void;
23
+ }
24
+ export {};