@testomatio/reporter 2.1.3-beta.1-multi-links → 2.1.3-beta.2-xml-import
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/lib/adapter/codecept.js +6 -5
- package/lib/adapter/mocha.js +14 -0
- package/lib/adapter/webdriver.js +6 -4
- package/lib/bin/startTest.js +38 -91
- package/lib/client.js +6 -3
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/pipe/testomatio.js +1 -2
- package/lib/reporter-functions.d.ts +20 -7
- package/lib/reporter-functions.js +27 -35
- package/lib/reporter.d.ts +22 -20
- package/lib/reporter.js +9 -7
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -2
- package/lib/services/index.js +2 -2
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +1 -1
- package/lib/services/labels.js +2 -2
- package/lib/services/logger.d.ts +1 -1
- package/lib/utils/utils.js +3 -1
- package/lib/xmlReader.d.ts +7 -0
- package/lib/xmlReader.js +231 -9
- package/package.json +1 -1
- package/src/adapter/codecept.js +6 -5
- package/src/adapter/mocha.js +15 -0
- package/src/adapter/webdriver.js +6 -4
- package/src/bin/startTest.js +43 -114
- package/src/client.js +5 -3
- package/src/data-storage.js +6 -6
- package/src/pipe/testomatio.js +1 -2
- package/src/reporter-functions.js +27 -37
- package/src/reporter.js +8 -6
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/services/links.js +69 -0
- package/src/utils/utils.js +5 -3
- package/src/xmlReader.js +267 -9
- package/lib/utils/cli_utils.d.ts +0 -1
- package/lib/utils/cli_utils.js +0 -65552
package/lib/reporter.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
export type artifact = typeof import("./reporter-functions.js");
|
|
2
1
|
export const artifact: (data: string | {
|
|
3
2
|
path: string;
|
|
4
3
|
type: string;
|
|
5
4
|
name: string;
|
|
6
5
|
}, context?: any) => void;
|
|
7
|
-
export type log = typeof import("./reporter-functions.js");
|
|
8
6
|
export const log: (...args: any[]) => void;
|
|
9
|
-
export type logger = typeof import("./services/index.js");
|
|
10
7
|
export const logger: {
|
|
11
|
-
"__#
|
|
8
|
+
"__#14@#originalUserLogger": {
|
|
12
9
|
assert(condition?: boolean, ...data: any[]): void;
|
|
13
10
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
14
11
|
clear(): void;
|
|
@@ -53,13 +50,13 @@ export const logger: {
|
|
|
53
50
|
profile(label?: string): void;
|
|
54
51
|
profileEnd(label?: string): void;
|
|
55
52
|
};
|
|
56
|
-
"__#
|
|
53
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
57
54
|
logLevel: string;
|
|
58
55
|
step(strings: any, ...values: any[]): void;
|
|
59
56
|
getLogs(context: string): string[];
|
|
60
|
-
"__#
|
|
57
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
61
58
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
62
|
-
"__#
|
|
59
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
63
60
|
assert(...args: any[]): void;
|
|
64
61
|
debug(...args: any[]): void;
|
|
65
62
|
error(...args: any[]): void;
|
|
@@ -75,17 +72,15 @@ export const logger: {
|
|
|
75
72
|
}): void;
|
|
76
73
|
prettyObjects: boolean;
|
|
77
74
|
};
|
|
78
|
-
export type meta = typeof import("./reporter-functions.js");
|
|
79
75
|
export const meta: (keyValue: {
|
|
80
76
|
[key: string]: string;
|
|
81
77
|
} | string, value?: string | null) => void;
|
|
82
|
-
export type step = typeof import("./reporter-functions.js");
|
|
83
78
|
export const step: (message: string) => void;
|
|
84
|
-
export
|
|
85
|
-
export const
|
|
79
|
+
export const label: (key: string, value?: string | string[] | null) => void;
|
|
80
|
+
export const linkTest: (...testIds: string[]) => void;
|
|
86
81
|
declare namespace _default {
|
|
87
82
|
let testomatioLogger: {
|
|
88
|
-
"__#
|
|
83
|
+
"__#14@#originalUserLogger": {
|
|
89
84
|
assert(condition?: boolean, ...data: any[]): void;
|
|
90
85
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
91
86
|
clear(): void;
|
|
@@ -130,13 +125,13 @@ declare namespace _default {
|
|
|
130
125
|
profile(label?: string): void;
|
|
131
126
|
profileEnd(label?: string): void;
|
|
132
127
|
};
|
|
133
|
-
"__#
|
|
128
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
134
129
|
logLevel: string;
|
|
135
130
|
step(strings: any, ...values: any[]): void;
|
|
136
131
|
getLogs(context: string): string[];
|
|
137
|
-
"__#
|
|
132
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
138
133
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
139
|
-
"__#
|
|
134
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
140
135
|
assert(...args: any[]): void;
|
|
141
136
|
debug(...args: any[]): void;
|
|
142
137
|
error(...args: any[]): void;
|
|
@@ -159,7 +154,7 @@ declare namespace _default {
|
|
|
159
154
|
}, context?: any) => void;
|
|
160
155
|
let log: (...args: any[]) => void;
|
|
161
156
|
let logger: {
|
|
162
|
-
"__#
|
|
157
|
+
"__#14@#originalUserLogger": {
|
|
163
158
|
assert(condition?: boolean, ...data: any[]): void;
|
|
164
159
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
165
160
|
clear(): void;
|
|
@@ -204,13 +199,13 @@ declare namespace _default {
|
|
|
204
199
|
profile(label?: string): void;
|
|
205
200
|
profileEnd(label?: string): void;
|
|
206
201
|
};
|
|
207
|
-
"__#
|
|
202
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
208
203
|
logLevel: string;
|
|
209
204
|
step(strings: any, ...values: any[]): void;
|
|
210
205
|
getLogs(context: string): string[];
|
|
211
|
-
"__#
|
|
206
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
212
207
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
213
|
-
"__#
|
|
208
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
214
209
|
assert(...args: any[]): void;
|
|
215
210
|
debug(...args: any[]): void;
|
|
216
211
|
error(...args: any[]): void;
|
|
@@ -230,6 +225,13 @@ declare namespace _default {
|
|
|
230
225
|
[key: string]: string;
|
|
231
226
|
} | string, value?: string | null) => void;
|
|
232
227
|
let step: (message: string) => void;
|
|
233
|
-
let label: (key: string, value?: string) => void;
|
|
228
|
+
let label: (key: string, value?: string | string[] | null) => void;
|
|
229
|
+
let linkTest: (...testIds: string[]) => void;
|
|
234
230
|
}
|
|
235
231
|
export default _default;
|
|
232
|
+
export type ArtifactFunction = typeof import("./reporter-functions.js").default.artifact;
|
|
233
|
+
export type LogFunction = typeof import("./reporter-functions.js").default.log;
|
|
234
|
+
export type LoggerService = typeof import("./services/index.js").services.logger;
|
|
235
|
+
export type MetaFunction = typeof import("./reporter-functions.js").default.keyValue;
|
|
236
|
+
export type StepFunction = typeof import("./reporter-functions.js").default.step;
|
|
237
|
+
export type LabelFunction = typeof import("./reporter-functions.js").default.label;
|
package/lib/reporter.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
|
|
6
|
+
exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
|
|
7
7
|
// import TestomatClient from './client.js';
|
|
8
8
|
// import * as TRConstants from './constants.js';
|
|
9
9
|
const index_js_1 = require("./services/index.js");
|
|
@@ -14,13 +14,14 @@ exports.logger = index_js_1.services.logger;
|
|
|
14
14
|
exports.meta = reporter_functions_js_1.default.keyValue;
|
|
15
15
|
exports.step = reporter_functions_js_1.default.step;
|
|
16
16
|
exports.label = reporter_functions_js_1.default.label;
|
|
17
|
+
exports.linkTest = reporter_functions_js_1.default.linkTest;
|
|
17
18
|
/**
|
|
18
|
-
* @typedef {import('./reporter-functions.js')}
|
|
19
|
-
* @typedef {import('./reporter-functions.js')}
|
|
20
|
-
* @typedef {import('./services/index.js')}
|
|
21
|
-
* @typedef {import('./reporter-functions.js')}
|
|
22
|
-
* @typedef {import('./reporter-functions.js')}
|
|
23
|
-
* @typedef {import('./reporter-functions.js')}
|
|
19
|
+
* @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
|
|
20
|
+
* @typedef {typeof import('./reporter-functions.js').default.log} LogFunction
|
|
21
|
+
* @typedef {typeof import('./services/index.js').services.logger} LoggerService
|
|
22
|
+
* @typedef {typeof import('./reporter-functions.js').default.keyValue} MetaFunction
|
|
23
|
+
* @typedef {typeof import('./reporter-functions.js').default.step} StepFunction
|
|
24
|
+
* @typedef {typeof import('./reporter-functions.js').default.label} LabelFunction
|
|
24
25
|
*/
|
|
25
26
|
module.exports = {
|
|
26
27
|
/**
|
|
@@ -33,6 +34,7 @@ module.exports = {
|
|
|
33
34
|
meta: reporter_functions_js_1.default.keyValue,
|
|
34
35
|
step: reporter_functions_js_1.default.step,
|
|
35
36
|
label: reporter_functions_js_1.default.label,
|
|
37
|
+
linkTest: reporter_functions_js_1.default.linkTest,
|
|
36
38
|
// TestomatClient,
|
|
37
39
|
// TRConstants,
|
|
38
40
|
};
|
package/lib/services/index.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ export namespace services {
|
|
|
2
2
|
export { logger };
|
|
3
3
|
export { artifactStorage as artifacts };
|
|
4
4
|
export { keyValueStorage as keyValues };
|
|
5
|
-
export {
|
|
5
|
+
export { linkStorage as links };
|
|
6
6
|
export function setContext(context: any): void;
|
|
7
7
|
}
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
9
|
import { artifactStorage } from './artifacts.js';
|
|
10
10
|
import { keyValueStorage } from './key-values.js';
|
|
11
|
-
import {
|
|
11
|
+
import { linkStorage } from './links.js';
|
package/lib/services/index.js
CHANGED
|
@@ -4,13 +4,13 @@ exports.services = void 0;
|
|
|
4
4
|
const logger_js_1 = require("./logger.js");
|
|
5
5
|
const artifacts_js_1 = require("./artifacts.js");
|
|
6
6
|
const key_values_js_1 = require("./key-values.js");
|
|
7
|
-
const
|
|
7
|
+
const links_js_1 = require("./links.js");
|
|
8
8
|
const data_storage_js_1 = require("../data-storage.js");
|
|
9
9
|
exports.services = {
|
|
10
10
|
logger: logger_js_1.logger,
|
|
11
11
|
artifacts: artifacts_js_1.artifactStorage,
|
|
12
12
|
keyValues: key_values_js_1.keyValueStorage,
|
|
13
|
-
|
|
13
|
+
links: links_js_1.linkStorage,
|
|
14
14
|
setContext: context => {
|
|
15
15
|
data_storage_js_1.dataStorage.setContext(context);
|
|
16
16
|
},
|
package/lib/services/labels.d.ts
CHANGED
package/lib/services/labels.js
CHANGED
|
@@ -27,7 +27,7 @@ class LabelStorage {
|
|
|
27
27
|
put(labels, context = null) {
|
|
28
28
|
if (!labels || !Array.isArray(labels))
|
|
29
29
|
return;
|
|
30
|
-
data_storage_js_1.dataStorage.putData('
|
|
30
|
+
data_storage_js_1.dataStorage.putData('links', labels, context);
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Returns labels array for the test
|
|
@@ -35,7 +35,7 @@ class LabelStorage {
|
|
|
35
35
|
* @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
|
|
36
36
|
*/
|
|
37
37
|
get(context = null) {
|
|
38
|
-
const labelsList = data_storage_js_1.dataStorage.getData('
|
|
38
|
+
const labelsList = data_storage_js_1.dataStorage.getData('links', context);
|
|
39
39
|
if (!labelsList || !labelsList?.length)
|
|
40
40
|
return [];
|
|
41
41
|
const allLabels = [];
|
package/lib/services/logger.d.ts
CHANGED
package/lib/utils/utils.js
CHANGED
|
@@ -321,7 +321,7 @@ const fileSystem = {
|
|
|
321
321
|
exports.fileSystem = fileSystem;
|
|
322
322
|
const foundedTestLog = (app, tests) => {
|
|
323
323
|
const n = tests.length;
|
|
324
|
-
return
|
|
324
|
+
return console.log(app, `✅ We found ${n === 1 ? 'one test' : `${n} tests`} in Testomat.io!`);
|
|
325
325
|
};
|
|
326
326
|
exports.foundedTestLog = foundedTestLog;
|
|
327
327
|
const humanize = text => {
|
|
@@ -399,6 +399,8 @@ function storeRunId(runId) {
|
|
|
399
399
|
function readLatestRunId() {
|
|
400
400
|
try {
|
|
401
401
|
const filePath = path_1.default.join(os_1.default.tmpdir(), `testomatio.latest.run`);
|
|
402
|
+
if (!fs_1.default.existsSync(filePath))
|
|
403
|
+
return null;
|
|
402
404
|
const stats = fs_1.default.statSync(filePath);
|
|
403
405
|
const diff = +new Date() - +stats.mtime;
|
|
404
406
|
const diffHours = diff / 1000 / 60 / 60;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ declare class XmlReader {
|
|
|
77
77
|
skipped_count: number;
|
|
78
78
|
tests: any[];
|
|
79
79
|
};
|
|
80
|
+
deduplicateTestsByFQN(tests: any): any[];
|
|
81
|
+
generateFQN(test: any): string;
|
|
82
|
+
generateNormalizedFQN(test: any): string;
|
|
83
|
+
extractAssemblyName(test: any): any;
|
|
84
|
+
extractNamespace(test: any): any;
|
|
85
|
+
extractClassName(test: any): any;
|
|
86
|
+
extractCsFileFromPath(test: any): any;
|
|
80
87
|
calculateStats(): {};
|
|
81
88
|
fetchSourceCode(): void;
|
|
82
89
|
formatTests(): void;
|
package/lib/xmlReader.js
CHANGED
|
@@ -131,7 +131,9 @@ class XmlReader {
|
|
|
131
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
134
|
-
|
|
134
|
+
// Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
|
|
135
|
+
const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
|
|
136
|
+
this.tests = this.tests.concat(deduplicatedTests);
|
|
135
137
|
return {
|
|
136
138
|
status: result?.toLowerCase(),
|
|
137
139
|
create_tests: true,
|
|
@@ -139,7 +141,7 @@ class XmlReader {
|
|
|
139
141
|
passed_count: parseInt(passed, 10),
|
|
140
142
|
failed_count: parseInt(failed, 10),
|
|
141
143
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
142
|
-
tests:
|
|
144
|
+
tests: deduplicatedTests,
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
processTRX(jsonSuite) {
|
|
@@ -275,6 +277,169 @@ class XmlReader {
|
|
|
275
277
|
tests,
|
|
276
278
|
};
|
|
277
279
|
}
|
|
280
|
+
deduplicateTestsByFQN(tests) {
|
|
281
|
+
const fqnMap = new Map();
|
|
282
|
+
tests.forEach(test => {
|
|
283
|
+
const fqn = this.generateNormalizedFQN(test);
|
|
284
|
+
if (fqnMap.has(fqn)) {
|
|
285
|
+
const existingTest = fqnMap.get(fqn);
|
|
286
|
+
// For parameterized tests, merge as Examples
|
|
287
|
+
if (test.example) {
|
|
288
|
+
// Initialize examples array if it doesn't exist
|
|
289
|
+
if (!existingTest.examples) {
|
|
290
|
+
existingTest.examples = [];
|
|
291
|
+
// Add the existing test's example as the first item
|
|
292
|
+
if (existingTest.example) {
|
|
293
|
+
existingTest.examples.push({
|
|
294
|
+
parameters: existingTest.example,
|
|
295
|
+
status: existingTest.status,
|
|
296
|
+
run_time: existingTest.run_time,
|
|
297
|
+
message: existingTest.message,
|
|
298
|
+
stack: existingTest.stack
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Add this test's execution as an example
|
|
303
|
+
existingTest.examples.push({
|
|
304
|
+
parameters: test.example,
|
|
305
|
+
status: test.status,
|
|
306
|
+
run_time: test.run_time,
|
|
307
|
+
message: test.message,
|
|
308
|
+
stack: test.stack
|
|
309
|
+
});
|
|
310
|
+
// Update the main test status to reflect the worst status
|
|
311
|
+
if (test.status === 'failed' || existingTest.status === 'failed') {
|
|
312
|
+
existingTest.status = 'failed';
|
|
313
|
+
}
|
|
314
|
+
else if (test.status === 'skipped' && existingTest.status !== 'failed') {
|
|
315
|
+
existingTest.status = 'skipped';
|
|
316
|
+
}
|
|
317
|
+
// Update total run time
|
|
318
|
+
existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
|
|
319
|
+
// Merge stack traces if they're different
|
|
320
|
+
if (test.stack && test.stack !== existingTest.stack) {
|
|
321
|
+
existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
|
|
322
|
+
}
|
|
323
|
+
// Merge messages if they're different
|
|
324
|
+
if (test.message && test.message !== existingTest.message) {
|
|
325
|
+
existingTest.message = existingTest.message + '; ' + test.message;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
// Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
|
|
330
|
+
if (test.test_id && !existingTest.test_id) {
|
|
331
|
+
existingTest.test_id = test.test_id;
|
|
332
|
+
}
|
|
333
|
+
// Keep the most complete test data
|
|
334
|
+
if (test.stack && !existingTest.stack) {
|
|
335
|
+
existingTest.stack = test.stack;
|
|
336
|
+
}
|
|
337
|
+
if (test.message && !existingTest.message) {
|
|
338
|
+
existingTest.message = test.message;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
342
|
+
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
343
|
+
existingTest.suite_title = test.suite_title;
|
|
344
|
+
existingTest.file = this.extractCsFileFromPath(test);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// Fix file path to use proper .cs file names from source paths
|
|
349
|
+
test.file = this.extractCsFileFromPath(test);
|
|
350
|
+
fqnMap.set(fqn, test);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return Array.from(fqnMap.values());
|
|
354
|
+
}
|
|
355
|
+
generateFQN(test) {
|
|
356
|
+
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
357
|
+
// Don't include assembly as it can vary between different test structures
|
|
358
|
+
const namespace = this.extractNamespace(test);
|
|
359
|
+
const className = this.extractClassName(test);
|
|
360
|
+
const methodName = test.title;
|
|
361
|
+
// Use the most complete namespace.class structure available
|
|
362
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
363
|
+
return `${test.suite_title}.${methodName}`;
|
|
364
|
+
}
|
|
365
|
+
return `${namespace}.${className}.${methodName}`;
|
|
366
|
+
}
|
|
367
|
+
generateNormalizedFQN(test) {
|
|
368
|
+
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
369
|
+
// For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
|
|
370
|
+
const fullClassName = test.suite_title || '';
|
|
371
|
+
const methodName = test.title;
|
|
372
|
+
// Extract the most specific namespace.class pattern
|
|
373
|
+
if (fullClassName.includes('.')) {
|
|
374
|
+
const parts = fullClassName.split('.');
|
|
375
|
+
if (parts.length >= 2) {
|
|
376
|
+
const className = parts[parts.length - 1];
|
|
377
|
+
// Look for common .NET namespace patterns and normalize them:
|
|
378
|
+
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
379
|
+
// Tests.MyClass -> Tests.MyClass
|
|
380
|
+
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
381
|
+
let normalizedNamespace = '';
|
|
382
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
383
|
+
const part = parts[i];
|
|
384
|
+
// Build namespace from right to left, excluding project names
|
|
385
|
+
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
386
|
+
// Found a test namespace, use it as the normalized namespace
|
|
387
|
+
normalizedNamespace = part;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
else if (i === parts.length - 2) {
|
|
391
|
+
// If no test namespace found, use the immediate parent as namespace
|
|
392
|
+
normalizedNamespace = part;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Fallback for simple class names
|
|
399
|
+
return `${fullClassName}.${methodName}`;
|
|
400
|
+
}
|
|
401
|
+
extractAssemblyName(test) {
|
|
402
|
+
// Extract assembly name from file path or use default
|
|
403
|
+
if (test.file) {
|
|
404
|
+
const parts = test.file.split(/[/\\]/);
|
|
405
|
+
return parts[0] || 'DefaultAssembly';
|
|
406
|
+
}
|
|
407
|
+
return 'DefaultAssembly';
|
|
408
|
+
}
|
|
409
|
+
extractNamespace(test) {
|
|
410
|
+
// Extract namespace from suite_title or classname
|
|
411
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
412
|
+
const parts = test.suite_title.split('.');
|
|
413
|
+
return parts.slice(0, -1).join('.');
|
|
414
|
+
}
|
|
415
|
+
return test.suite_title || 'DefaultNamespace';
|
|
416
|
+
}
|
|
417
|
+
extractClassName(test) {
|
|
418
|
+
// Extract class name from suite_title
|
|
419
|
+
if (test.suite_title && test.suite_title.includes('.')) {
|
|
420
|
+
const parts = test.suite_title.split('.');
|
|
421
|
+
return parts[parts.length - 1];
|
|
422
|
+
}
|
|
423
|
+
return test.suite_title || 'DefaultClass';
|
|
424
|
+
}
|
|
425
|
+
extractCsFileFromPath(test) {
|
|
426
|
+
// Extract .cs file name from source file path, not namespace
|
|
427
|
+
if (test.file) {
|
|
428
|
+
// Look for actual .cs file path patterns
|
|
429
|
+
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
430
|
+
if (csFileMatch) {
|
|
431
|
+
return test.file;
|
|
432
|
+
}
|
|
433
|
+
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
434
|
+
const className = this.extractClassName(test);
|
|
435
|
+
const pathParts = test.file.split(/[/\\]/);
|
|
436
|
+
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
437
|
+
return pathParts.join('/');
|
|
438
|
+
}
|
|
439
|
+
// Fallback to class name
|
|
440
|
+
const className = this.extractClassName(test);
|
|
441
|
+
return `${className}.cs`;
|
|
442
|
+
}
|
|
278
443
|
calculateStats() {
|
|
279
444
|
this.stats = {
|
|
280
445
|
...this.stats,
|
|
@@ -430,7 +595,8 @@ function reduceTestCases(prev, item) {
|
|
|
430
595
|
testCases
|
|
431
596
|
.filter(t => !!t)
|
|
432
597
|
.forEach(testCaseItem => {
|
|
433
|
-
|
|
598
|
+
// Use consistent Test Explorer structure: prioritize fullname for file path
|
|
599
|
+
const file = extractSourceFilePath(testCaseItem, item);
|
|
434
600
|
let stack = '';
|
|
435
601
|
let message = '';
|
|
436
602
|
if (testCaseItem.error)
|
|
@@ -450,16 +616,20 @@ function reduceTestCases(prev, item) {
|
|
|
450
616
|
if (!message)
|
|
451
617
|
message = stack.trim().split('\n')[0];
|
|
452
618
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
453
|
-
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
454
619
|
// SpecFlow config
|
|
455
620
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
456
621
|
let example = null;
|
|
457
|
-
|
|
622
|
+
// Use consistent Test Explorer structure for suite title
|
|
623
|
+
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
458
624
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
459
625
|
tags ||= [];
|
|
460
|
-
|
|
626
|
+
// Store original test name for parameter extraction
|
|
627
|
+
const originalTestName = testCaseItem.name || testCaseItem.methodname;
|
|
628
|
+
const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
|
|
461
629
|
if (exampleMatches) {
|
|
462
|
-
|
|
630
|
+
// Extract and store parameters as Examples
|
|
631
|
+
const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
632
|
+
example = { ...parameterValues };
|
|
463
633
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
464
634
|
}
|
|
465
635
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
@@ -508,6 +678,7 @@ function reduceTestCases(prev, item) {
|
|
|
508
678
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
509
679
|
status,
|
|
510
680
|
title,
|
|
681
|
+
originalTestName, // Store original name for parameter-aware FQN generation
|
|
511
682
|
root_suite_id: TESTOMATIO_SUITE,
|
|
512
683
|
suite_title: suiteTitle,
|
|
513
684
|
files,
|
|
@@ -516,6 +687,51 @@ function reduceTestCases(prev, item) {
|
|
|
516
687
|
});
|
|
517
688
|
return prev;
|
|
518
689
|
}
|
|
690
|
+
function extractSourceFilePath(testCaseItem, item) {
|
|
691
|
+
// Priority order for file path extraction to match Test Explorer structure:
|
|
692
|
+
// 1. fullname (contains full project path)
|
|
693
|
+
// 2. filepath (direct file path)
|
|
694
|
+
// 3. file attribute from test case
|
|
695
|
+
// 4. package (fallback)
|
|
696
|
+
if (item.fullname) {
|
|
697
|
+
// Extract actual file path from fullname if it contains path separators
|
|
698
|
+
const fullnameParts = item.fullname.split('.');
|
|
699
|
+
if (fullnameParts.length > 2) {
|
|
700
|
+
// Reconstruct path from project.namespace.class structure
|
|
701
|
+
const projectName = fullnameParts[0];
|
|
702
|
+
const namespaceParts = fullnameParts.slice(1, -1);
|
|
703
|
+
const className = fullnameParts[fullnameParts.length - 1];
|
|
704
|
+
return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (item.filepath)
|
|
708
|
+
return item.filepath;
|
|
709
|
+
if (testCaseItem.file)
|
|
710
|
+
return testCaseItem.file;
|
|
711
|
+
if (item.package)
|
|
712
|
+
return item.package;
|
|
713
|
+
// Fallback: construct from classname
|
|
714
|
+
if (testCaseItem.classname) {
|
|
715
|
+
const parts = testCaseItem.classname.split('.');
|
|
716
|
+
const className = parts[parts.length - 1];
|
|
717
|
+
const namespacePath = parts.slice(0, -1).join('/');
|
|
718
|
+
return `${namespacePath}/${className}.cs`;
|
|
719
|
+
}
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
723
|
+
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
724
|
+
// Priority: fullname > classname > name
|
|
725
|
+
if (item.fullname) {
|
|
726
|
+
// Use fullname to maintain Test Explorer structure
|
|
727
|
+
return item.fullname;
|
|
728
|
+
}
|
|
729
|
+
if (testCaseItem.classname) {
|
|
730
|
+
return testCaseItem.classname;
|
|
731
|
+
}
|
|
732
|
+
// Fallback to item name but prefer classname structure
|
|
733
|
+
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
734
|
+
}
|
|
519
735
|
function processTestSuite(testsuite) {
|
|
520
736
|
if (!testsuite)
|
|
521
737
|
return [];
|
|
@@ -527,8 +743,14 @@ function processTestSuite(testsuite) {
|
|
|
527
743
|
if (!Array.isArray(testsuite)) {
|
|
528
744
|
suites = [testsuite];
|
|
529
745
|
}
|
|
530
|
-
|
|
531
|
-
|
|
746
|
+
// Only process suites that have test cases OR child suites, but avoid double processing
|
|
747
|
+
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
748
|
+
const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
|
|
749
|
+
// Process child suites recursively
|
|
750
|
+
const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
|
|
751
|
+
// Process leaf suites with actual test cases
|
|
752
|
+
const leafResults = leafSuites.reduce(reduceTestCases, []);
|
|
753
|
+
return [...childResults, ...leafResults];
|
|
532
754
|
}
|
|
533
755
|
function fetchProperties(item) {
|
|
534
756
|
const tags = [];
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -56,19 +56,20 @@ function CodeceptReporter(config) {
|
|
|
56
56
|
|
|
57
57
|
output.debug = function(msg) {
|
|
58
58
|
originalOutput.debug(msg);
|
|
59
|
-
dataStorage.putData('log', repeat(this
|
|
59
|
+
dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.cyan(msg.toString()));
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
output.say = function(message, color = 'cyan') {
|
|
63
63
|
originalOutput.say(message, color);
|
|
64
|
-
const sayMsg = repeat(this
|
|
64
|
+
const sayMsg = repeat(this?.stepShift || 0) + ` ${pc.bold(pc[color](message))}`;
|
|
65
65
|
dataStorage.putData('log', sayMsg);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
output.log = function(msg) {
|
|
69
69
|
originalOutput.log(msg);
|
|
70
|
-
dataStorage.putData('log', repeat(this
|
|
70
|
+
dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.gray(msg));
|
|
71
71
|
};
|
|
72
|
+
output.stepShift = 0;
|
|
72
73
|
|
|
73
74
|
recorder.startUnlessRunning();
|
|
74
75
|
|
|
@@ -162,7 +163,7 @@ function CodeceptReporter(config) {
|
|
|
162
163
|
const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
|
|
163
164
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
164
165
|
const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
|
|
165
|
-
const
|
|
166
|
+
const links = services.links.get(test.fullTitle());
|
|
166
167
|
|
|
167
168
|
services.setContext(null);
|
|
168
169
|
|
|
@@ -177,7 +178,7 @@ function CodeceptReporter(config) {
|
|
|
177
178
|
files,
|
|
178
179
|
steps: stepHierarchy, // Array of step objects per API schema
|
|
179
180
|
logs,
|
|
180
|
-
|
|
181
|
+
links,
|
|
181
182
|
manuallyAttachedArtifacts,
|
|
182
183
|
meta: { ...keyValues, ...test.meta },
|
|
183
184
|
});
|