qase-javascript-commons 2.5.0 → 2.5.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/changelog.md CHANGED
@@ -1,3 +1,24 @@
1
+ # qase-javascript-commons@2.5.2
2
+
3
+ ## What's new
4
+
5
+ Aligned report output with the cross-language Qase Report specification:
6
+
7
+ - Report file renamed from `report.json` to `run.json`, title changed to `"Test run"`.
8
+ - Stats model: replaced `broken` field with `blocked` and `invalid`, fixed status enum mapping.
9
+ - Result serialization: `testops_id` serializes as `testops_ids` (array), `group_params` as `param_groups` (array of arrays).
10
+ - Step serialization: `data` field serializes as `input_data`, attachments moved to `execution.attachments`.
11
+ - Gherkin-type steps automatically convert to TEXT format during serialization (`keyword + name` → `action`).
12
+ - Attachment `size` and `content` fields excluded from serialized output.
13
+
14
+ All changes are serialization-only — internal model interfaces are unchanged, no breaking changes for framework reporters.
15
+
16
+ # qase-javascript-commons@2.5.1
17
+
18
+ ## What's new
19
+
20
+ Fixed issue with incorrect test status determination.
21
+
1
22
  # qase-javascript-commons@2.5.0
2
23
 
3
24
  ## What's new
@@ -2,7 +2,8 @@ export interface Stats {
2
2
  passed: number;
3
3
  failed: number;
4
4
  skipped: number;
5
- broken: number;
5
+ blocked: number;
6
+ invalid: number;
6
7
  muted: number;
7
8
  total: number;
8
9
  }
@@ -41,4 +41,38 @@ export declare class ReportReporter extends AbstractReporter {
41
41
  * @returns {TestStepType[]}
42
42
  */
43
43
  private copyStepAttachments;
44
+ /**
45
+ * Serialize a test result to spec-compliant JSON format.
46
+ * Transforms internal model fields to match the Qase Report specification.
47
+ * @private
48
+ */
49
+ private serializeResultForReport;
50
+ /**
51
+ * Transform group_params Record to param_groups array of arrays.
52
+ * Same logic as clientV2.ts transformGroupParams.
53
+ * @private
54
+ */
55
+ private transformGroupParams;
56
+ /**
57
+ * Serialize attachment for report output (exclude size and content fields).
58
+ * @private
59
+ */
60
+ private serializeAttachment;
61
+ /**
62
+ * Serialize steps recursively, transforming:
63
+ * - data.data -> data.input_data (STEP-01)
64
+ * - attachments -> execution.attachments (STEP-02)
65
+ * @private
66
+ */
67
+ private serializeSteps;
68
+ /**
69
+ * Serialize a single step to spec-compliant format.
70
+ * @private
71
+ */
72
+ private serializeStep;
73
+ /**
74
+ * Serialize step data, transforming data.data -> data.input_data for text steps.
75
+ * @private
76
+ */
77
+ private serializeStepData;
44
78
  }
@@ -76,12 +76,14 @@ class ReportReporter extends abstract_reporter_1.AbstractReporter {
76
76
  },
77
77
  };
78
78
  }
79
- await this.writer.writeTestResult(result);
79
+ // Serialize to spec-compliant format before writing
80
+ const serialized = this.serializeResultForReport(result);
81
+ await this.writer.writeTestResult(serialized);
80
82
  }
81
83
  }
82
84
  async complete() {
83
85
  const report = {
84
- title: 'Test report',
86
+ title: 'Test run',
85
87
  execution: {
86
88
  start_time: this.startTime,
87
89
  end_time: Date.now(),
@@ -93,7 +95,8 @@ class ReportReporter extends abstract_reporter_1.AbstractReporter {
93
95
  passed: 0,
94
96
  failed: 0,
95
97
  skipped: 0,
96
- broken: 0,
98
+ blocked: 0,
99
+ invalid: 0,
97
100
  muted: 0,
98
101
  },
99
102
  results: [],
@@ -115,10 +118,10 @@ class ReportReporter extends abstract_reporter_1.AbstractReporter {
115
118
  report.stats.skipped++;
116
119
  break;
117
120
  case models_1.TestStatusEnum.invalid:
118
- report.stats.broken++;
121
+ report.stats.invalid++;
119
122
  break;
120
123
  case models_1.TestStatusEnum.blocked:
121
- report.stats.muted++;
124
+ report.stats.blocked++;
122
125
  break;
123
126
  }
124
127
  report.execution.cumulative_duration += result.execution.duration ?? 0;
@@ -152,5 +155,128 @@ class ReportReporter extends abstract_reporter_1.AbstractReporter {
152
155
  }
153
156
  return steps;
154
157
  }
158
+ /**
159
+ * Serialize a test result to spec-compliant JSON format.
160
+ * Transforms internal model fields to match the Qase Report specification.
161
+ * @private
162
+ */
163
+ serializeResultForReport(result) {
164
+ // Transform testops_id -> testops_ids (RSLT-01)
165
+ let testopsIds = null;
166
+ if (result.testops_id !== null) {
167
+ testopsIds = Array.isArray(result.testops_id)
168
+ ? result.testops_id
169
+ : [result.testops_id];
170
+ }
171
+ // Transform group_params -> param_groups (RSLT-02)
172
+ const paramGroups = this.transformGroupParams(result.group_params);
173
+ // Serialize attachments (exclude size and content fields)
174
+ const attachments = result.attachments.map(att => this.serializeAttachment(att));
175
+ // Serialize steps (handle data.data -> data.input_data and attachments -> execution.attachments)
176
+ const steps = this.serializeSteps(result.steps);
177
+ // Build spec-compliant result object
178
+ const serialized = {
179
+ id: result.id,
180
+ title: result.title,
181
+ signature: result.signature,
182
+ execution: result.execution,
183
+ fields: result.fields,
184
+ attachments: attachments,
185
+ steps: steps,
186
+ params: result.params,
187
+ param_groups: paramGroups,
188
+ testops_ids: testopsIds,
189
+ relations: result.relations,
190
+ muted: result.muted,
191
+ message: result.message,
192
+ };
193
+ // Internal-only fields are excluded: testops_id, group_params, run_id, author, testops_project_mapping, preparedAttachments
194
+ return serialized;
195
+ }
196
+ /**
197
+ * Transform group_params Record to param_groups array of arrays.
198
+ * Same logic as clientV2.ts transformGroupParams.
199
+ * @private
200
+ */
201
+ transformGroupParams(groupParams) {
202
+ const keys = Object.keys(groupParams);
203
+ if (keys.length === 0) {
204
+ return [];
205
+ }
206
+ return [keys];
207
+ }
208
+ /**
209
+ * Serialize attachment for report output (exclude size and content fields).
210
+ * @private
211
+ */
212
+ serializeAttachment(att) {
213
+ return {
214
+ id: att.id,
215
+ file_name: att.file_name,
216
+ mime_type: att.mime_type,
217
+ file_path: att.file_path,
218
+ };
219
+ }
220
+ /**
221
+ * Serialize steps recursively, transforming:
222
+ * - data.data -> data.input_data (STEP-01)
223
+ * - attachments -> execution.attachments (STEP-02)
224
+ * @private
225
+ */
226
+ serializeSteps(steps) {
227
+ return steps.map(step => this.serializeStep(step));
228
+ }
229
+ /**
230
+ * Serialize a single step to spec-compliant format.
231
+ * @private
232
+ */
233
+ serializeStep(step) {
234
+ // Transform step data (handle data.data -> data.input_data for text steps)
235
+ const data = this.serializeStepData(step);
236
+ // Serialize step attachments
237
+ const attachments = step.attachments.map(att => this.serializeAttachment(att));
238
+ // Move attachments into execution.attachments (STEP-02)
239
+ const execution = {
240
+ ...step.execution,
241
+ attachments: attachments,
242
+ };
243
+ // Recursively serialize nested steps
244
+ const nestedSteps = this.serializeSteps(step.steps);
245
+ return {
246
+ id: step.id,
247
+ step_type: step.step_type,
248
+ data: data,
249
+ parent_id: step.parent_id,
250
+ execution: execution,
251
+ steps: nestedSteps,
252
+ // Note: attachments field is NOT at top-level in serialized output
253
+ };
254
+ }
255
+ /**
256
+ * Serialize step data, transforming data.data -> data.input_data for text steps.
257
+ * @private
258
+ */
259
+ serializeStepData(step) {
260
+ const data = { ...step.data };
261
+ // For text steps, rename data.data to data.input_data (STEP-01)
262
+ if (step.step_type === models_1.StepType.TEXT && 'data' in data) {
263
+ const textData = data;
264
+ return {
265
+ action: textData.action,
266
+ expected_result: textData.expected_result,
267
+ input_data: textData.data, // Rename: data -> input_data
268
+ };
269
+ }
270
+ // For gherkin steps, convert to text format (STEP-03)
271
+ if (step.step_type === models_1.StepType.GHERKIN && 'keyword' in data && 'name' in data) {
272
+ const gherkinData = data;
273
+ return {
274
+ action: `${gherkinData.keyword} ${gherkinData.name}`,
275
+ expected_result: null,
276
+ input_data: null, // JS GherkinData has no data field
277
+ };
278
+ }
279
+ return data;
280
+ }
155
281
  }
156
282
  exports.ReportReporter = ReportReporter;
@@ -14,7 +14,7 @@ function determineTestStatus(error, originalStatus) {
14
14
  return mapOriginalStatus(originalStatus);
15
15
  }
16
16
  // Check if it's an assertion error
17
- if (isAssertionError(error)) {
17
+ if (isAssertionError(error, originalStatus)) {
18
18
  return test_execution_1.TestStatusEnum.failed;
19
19
  }
20
20
  // For all other errors, return invalid
@@ -23,16 +23,19 @@ function determineTestStatus(error, originalStatus) {
23
23
  /**
24
24
  * Checks if error is an assertion error
25
25
  * @param error - Error object
26
+ * @param originalStatus - Original test status from test runner
26
27
  * @returns boolean - true if assertion error
27
28
  */
28
- function isAssertionError(error) {
29
+ function isAssertionError(error, originalStatus) {
29
30
  const errorMessage = error.message.toLowerCase();
30
31
  const errorStack = error.stack?.toLowerCase() || '';
32
+ const normalizedOriginalStatus = originalStatus.toLowerCase();
31
33
  // Common assertion error patterns
32
34
  const assertionPatterns = [
33
35
  'expect',
34
36
  'assert',
35
37
  'matcher',
38
+ 'objectcontaining',
36
39
  'assertion',
37
40
  'expected',
38
41
  'actual',
@@ -77,6 +80,14 @@ function isAssertionError(error) {
77
80
  const nonAssertionPatternsWithoutTimeout = nonAssertionPatterns.filter(pattern => pattern !== 'timeout');
78
81
  const hasNonAssertionPattern = nonAssertionPatternsWithoutTimeout.some(pattern => errorMessage.includes(pattern) || errorStack.includes(pattern));
79
82
  if (hasNonAssertionPattern) {
83
+ const hasAssertionContext = assertionPatterns.some(pattern => errorMessage.includes(pattern) || errorStack.includes(pattern));
84
+ const isRunnerFailed = normalizedOriginalStatus === 'failed' || normalizedOriginalStatus === 'timedout' || normalizedOriginalStatus === 'interrupted';
85
+ const isSyntaxError = errorMessage.includes('syntaxerror') || errorStack.includes('syntaxerror');
86
+ // When runner reported failure and error has assertion context (expect, ObjectContaining, diff),
87
+ // treat as Failed. Exception: SyntaxError (e.g. "Unexpected token") contains "expected" as false positive.
88
+ if (isRunnerFailed && hasAssertionContext && !isSyntaxError) {
89
+ return true;
90
+ }
80
91
  return false;
81
92
  }
82
93
  // For timeout errors without expect, treat as invalid
@@ -99,6 +110,7 @@ function isAssertionError(error) {
99
110
  * @returns TestStatusEnum
100
111
  */
101
112
  function mapOriginalStatus(originalStatus) {
113
+ // Keys must be lowercase to match normalizedStatus (case-insensitive matching)
102
114
  const statusMap = {
103
115
  'passed': test_execution_1.TestStatusEnum.passed,
104
116
  'failed': test_execution_1.TestStatusEnum.failed,
@@ -107,10 +119,9 @@ function mapOriginalStatus(originalStatus) {
107
119
  'pending': test_execution_1.TestStatusEnum.skipped,
108
120
  'todo': test_execution_1.TestStatusEnum.disabled,
109
121
  'focused': test_execution_1.TestStatusEnum.passed,
110
- 'timedOut': test_execution_1.TestStatusEnum.failed,
122
+ 'timedout': test_execution_1.TestStatusEnum.failed,
111
123
  'interrupted': test_execution_1.TestStatusEnum.failed,
112
124
  };
113
- // Convert to lowercase for case-insensitive matching
114
125
  const normalizedStatus = originalStatus.toLowerCase();
115
126
  return statusMap[normalizedStatus] || test_execution_1.TestStatusEnum.skipped;
116
127
  }
@@ -103,7 +103,7 @@ class FsWriter {
103
103
  }
104
104
  catch (error) { /* ignore */
105
105
  }
106
- const filePath = path.join(this.path, `report.${this.format}`);
106
+ const filePath = path.join(this.path, `run.${this.format}`);
107
107
  (0, fs_1.writeFileSync)(filePath, await this.formatter.format(results));
108
108
  return filePath;
109
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qase-javascript-commons",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "Qase JS Reporters",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  "@types/minimatch": "^6.0.0",
49
49
  "@types/node": "^20.19.25",
50
50
  "@types/uuid": "^9.0.8",
51
- "axios": "^1.13.2",
51
+ "axios": "^1.13.5",
52
52
  "jest": "^29.7.0",
53
53
  "ts-jest": "^29.4.5"
54
54
  }