@testomatio/reporter 0.5.8 → 0.6.0-beta.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/Changelog.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.5.10
2
+
3
+ * Fixed reporting Scenario Outline in Cypress-Cucumber
4
+ * Fixed error reports for Cypress when running in Chrome
5
+
6
+ # 0.5.9
7
+
8
+ * Added environment on Cypress report
9
+
1
10
  # 0.5.8
2
11
 
3
12
  * Fixed Cypress.io reporting
package/README.md CHANGED
@@ -205,6 +205,24 @@ Run the following command from you project folder:
205
205
  TESTOMATIO={API_KEY} npx @testomatio/reporter -c 'npx wdio wdio.conf.js'
206
206
  ```
207
207
 
208
+ ## JUnit Reports
209
+
210
+ Other frameworks and languages are supported via JUnit reports.
211
+
212
+ JUnit XML format is standard among test runners on various platforms. Testomat.io can load XML reports from test runners and create tests in a project from it. If your framework is not supported yet, generate JUnit report and upload it into Testomat.io
213
+
214
+ ```
215
+ TESTOMATIO={API_KEY} npx @testomatio/reporter -c 'npx wdio wdio.conf.js'
216
+ ```
217
+
218
+ Tested Frameworks:
219
+
220
+ * JUnit (JUnit)
221
+ * Python (Pytest)
222
+ * Minitest (Ruby)
223
+ * PHPUnit (PHP)
224
+
225
+
208
226
  ## Advanced Usage
209
227
 
210
228
  ### Create Unmatched Tests
@@ -3,13 +3,13 @@ const { parseTest, parseSuite } = require('../../util');
3
3
  const TestomatClient = require('../../client');
4
4
 
5
5
  const testomatioReporter = on => {
6
+ if (!process.env.TESTOMATIO) {
7
+ console.log('TESTOMATIO key is empty, ignoring reports');
8
+ return
9
+ }
6
10
  const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
7
11
 
8
12
  on('before:run', async (run) => {
9
- if (!client.title) {
10
- client.title = `${run.config.projectName} tests`
11
- if (client.title.includes('undefined')) delete client.title;
12
- }
13
13
  if (!client.env) {
14
14
  client.env = `${run.browser.displayName},${run.system.osName}`
15
15
  }
@@ -28,7 +28,12 @@ const testomatioReporter = on => {
28
28
  const time = latestAttempt.duration;
29
29
  const error = latestAttempt.error;
30
30
 
31
- const title = test.title.pop();
31
+ let title = test.title.pop();
32
+ let examples = title.match(/\(example (#\d+)\)/);
33
+ let example = null;
34
+ if (examples && examples[1]) example = { example: examples[1] };
35
+ title = title.replace(/\(example #\d+\)/, '').trim();
36
+
32
37
  const suiteTitle = test.title.pop();
33
38
 
34
39
  const testId = parseTest(title);
@@ -36,7 +41,10 @@ const testomatioReporter = on => {
36
41
 
37
42
  if (error) {
38
43
  error.inspect = function() {
39
- return this.codeFrame.frame;
44
+ if (this && this.codeFrame) {
45
+ return this.codeFrame.frame;
46
+ }
47
+ return '';
40
48
  }
41
49
  }
42
50
 
@@ -50,7 +58,7 @@ const testomatioReporter = on => {
50
58
  const state = test.state === 'passed' ? TRConstants.PASSED : TRConstants.FAILED;
51
59
 
52
60
  addSpecTestsPromises.push(client.addTestRun(testId, state, {
53
- title, time, error, files, suite_title: suiteTitle, suite_id: suiteId
61
+ title, time, example, error, files, suite_title: suiteTitle, suite_id: suiteId
54
62
  }));
55
63
  }
56
64
 
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ const program = require("commander");
3
+ // const chalk = require("chalk");
4
+ const glob = require('glob');
5
+
6
+ const { APP_PREFIX } = require('../constants');
7
+ const XmlReader = require("../xmlReader");
8
+
9
+ // const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || process.env.TESTOMATIO;
10
+ // const title = process.env.TESTOMATIO_TITLE;
11
+
12
+ // console.log(chalk.cyan.bold(` 🤩 xmlReader by Testomat.io v${version}`));
13
+
14
+ program
15
+ .arguments("<pattern>")
16
+ .option("-d, --dir <dir>", "Project directory")
17
+ .option("--java-tests [java-path]", "Load Java tests from path, by default: src/test/java")
18
+ .option("--lang <lang>", "Language used (python, ruby, java)")
19
+ .action(async (pattern, opts) => {
20
+ if (!pattern.endsWith('.xml')) {
21
+ pattern += '.xml';
22
+ }
23
+ let { javaTests, lang } = opts;
24
+ if (javaTests === true) javaTests = 'src/test/java';
25
+ const runReader = new XmlReader({ javaTests, lang });
26
+ const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
27
+ if (!files.length) {
28
+ console.log(APP_PREFIX,`Report can't be created. No XML files found 😥`);
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+
33
+ for (const file of files) {
34
+ console.log(APP_PREFIX,`Parsed ${file}`);
35
+ runReader.parse(file);
36
+ }
37
+ try {
38
+ await runReader.createRun();
39
+ await runReader.uploadData();
40
+ } catch (err) {
41
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
42
+ }
43
+ });
44
+
45
+
46
+ if (process.argv.length < 1) {
47
+ program.outputHelp();
48
+ }
49
+
50
+ program.parse(process.argv);
package/lib/client.js CHANGED
@@ -25,6 +25,7 @@ class TestomatClient {
25
25
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
26
26
  this.title = params.title || process.env.TESTOMATIO_TITLE;
27
27
  this.createNewTests = !!process.env.TESTOMATIO_CREATE
28
+ this.env = TESTOMATIO_ENV;
28
29
  this.parallel = params.parallel;
29
30
  this.runId = process.env.runId;
30
31
  this.queue = Promise.resolve();
@@ -45,8 +46,8 @@ class TestomatClient {
45
46
  api_key: this.apiKey.trim(),
46
47
  title: this.title,
47
48
  parallel: this.parallel,
49
+ env: this.env,
48
50
  group_title: TESTOMATIO_RUNGROUP_TITLE,
49
- env: TESTOMATIO_ENV,
50
51
  };
51
52
 
52
53
  global.testomatioArtifacts = [];
@@ -233,7 +234,7 @@ class TestomatClient {
233
234
 
234
235
  formatError(error, message) {
235
236
  if (!message) message = error.message;
236
- if (error.inspect) message = error.inspect();
237
+ if (error.inspect) message = error.inspect() || '';
237
238
 
238
239
  let stack = `\n${chalk.bold(message)}\n`;
239
240
 
@@ -0,0 +1,25 @@
1
+ class Adapter {
2
+
3
+ constructor(opts) {
4
+ this.opts = opts;
5
+ }
6
+
7
+ getFilePath(t) {
8
+ return t.file;
9
+ }
10
+
11
+ formatTest(t) {
12
+ return t;
13
+ }
14
+
15
+ formatStack(t) {
16
+ return t.stack || '';
17
+ }
18
+
19
+ formatMessage(t) {
20
+ return t.message;
21
+ }
22
+
23
+ }
24
+
25
+ module.exports = Adapter;
@@ -0,0 +1,40 @@
1
+ const path = require('path');
2
+ const Adapter = require('./adapter');
3
+
4
+ class JavaAdapter extends Adapter {
5
+
6
+ getFilePath(t) {
7
+ const fileName = namespaceToFileName(t.suite_title)
8
+ return this.opts.javaTests + path.sep + fileName;
9
+ }
10
+
11
+ formatTest(t) {
12
+ const fileParts = t.suite_title.split('.')
13
+ const example = t.title.match(/\[(.*)\]/)?.[1];
14
+ if (example) t.example = { "#": example }
15
+
16
+ t.file = namespaceToFileName(t.suite_title);
17
+ t.title = t.title.split('(')[0];
18
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
19
+ return t;
20
+ }
21
+
22
+ formatStack(t) {
23
+ let stack = super.formatStack(t);
24
+
25
+ const file = t.suite_title.split('.');
26
+
27
+ const fileLine = `at .*${file[file.length - 1]}\.java\:(\\d+)`
28
+ const regexp = new RegExp(fileLine,"g")
29
+ return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
30
+ }
31
+ }
32
+
33
+ function namespaceToFileName(fileName) {
34
+ let fileParts = fileName.split('.')
35
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '')
36
+ return fileParts.join(path.sep) + '.java';
37
+
38
+ }
39
+
40
+ module.exports = JavaAdapter;
@@ -0,0 +1,31 @@
1
+ const createCallsiteRecord = require('callsite-record');
2
+ const path = require('path');
3
+ const Adapter = require('./adapter');
4
+
5
+ class JavaScriptAdapter extends Adapter {
6
+
7
+ formatStack(t) {
8
+ let stack = super.formatStack(t);
9
+
10
+ try {
11
+ const error = new Error(stack.split('\n')[0])
12
+ error.stack = stack;
13
+ const record = createCallsiteRecord({
14
+ forError: error,
15
+ });
16
+ if (record && !record.filename.startsWith('http')) {
17
+ stack += record.renderSync({
18
+ stackFilter: frame =>
19
+ frame.getFileName().indexOf(path.sep) > -1 &&
20
+ frame.getFileName().indexOf('node_modules') < 0 &&
21
+ frame.getFileName().indexOf('internal') < 0,
22
+ });
23
+ }
24
+ return stack;
25
+ } catch (err) {
26
+ return stack;
27
+ }
28
+ }
29
+ }
30
+
31
+ module.exports = JavaScriptAdapter;
@@ -0,0 +1,42 @@
1
+ const path = require('path');
2
+ const Adapter = require('./adapter');
3
+ const fs = require('fs');
4
+
5
+ class PythonAdapter extends Adapter {
6
+
7
+ getFilePath(t) {
8
+ const fileName = namespaceToFileName(t.suite_title)
9
+ return fileName;
10
+ }
11
+
12
+ formatTest(t) {
13
+ const fileParts = t.suite_title.split('.')
14
+ const example = t.title.match(/\[(.*)\]/)?.[1];
15
+ if (example) t.example = { "#": example }
16
+
17
+ t.file = namespaceToFileName(t.suite_title);
18
+ t.title = t.title.split('[')[0];
19
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
20
+ return t;
21
+ }
22
+
23
+ formatMessage(t) {
24
+ return t.message.split('&#10;')[0];
25
+ }
26
+
27
+ }
28
+
29
+ function namespaceToFileName(fileName) {
30
+ let fileParts = fileName.split('.')
31
+
32
+ while (fileParts.length > 0) {
33
+ const file = fileParts.join(path.sep) + '.py';
34
+ if (fs.existsSync(fileParts.join(path.sep) + '.py')) {
35
+ return file;
36
+ }
37
+ fileParts.pop();
38
+ }
39
+ return null;
40
+ }
41
+
42
+ module.exports = PythonAdapter;
@@ -0,0 +1,13 @@
1
+ const path = require('path');
2
+ const Adapter = require('./adapter');
3
+ const fs = require('fs');
4
+
5
+ class RubyAdapter extends Adapter {
6
+
7
+ formatStack(t) {
8
+ const stack = super.formatStack(t);
9
+ return stack.replace(/\[(.*?\:.\d*)\]/g, '\n$1')
10
+ }
11
+ }
12
+
13
+ module.exports = RubyAdapter;
package/lib/util.js CHANGED
@@ -1,4 +1,10 @@
1
+ const { fstat } = require('fs');
2
+ const { includes, split } = require('lodash');
1
3
  const { URL } = require('url');
4
+ const { sep } = require('path');
5
+ const chalk = require('chalk');
6
+ const fs = require('fs');
7
+ const isValid = require('is-valid-path');
2
8
 
3
9
  /**
4
10
  * @param {String} testTitle - Test title
@@ -48,9 +54,98 @@ const isValidUrl = s => {
48
54
  }
49
55
  };
50
56
 
57
+ const fetchFilesFromStackTrace = (stack = '') => {
58
+ const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
59
+ return Array.from(files).map(f => f[1]).filter(f => fs.existsSync(f));
60
+ }
61
+
62
+ const fetchSourceCodeFromStackTrace = (stack = '') => {
63
+ const stackLines = stack.split('\n')
64
+ .filter(l => l.includes(':'))
65
+ // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
66
+ // .map(l => l.split(':')[0])
67
+ .map(l => l.trim())
68
+ .map(l => l.split(' ').find(p => p.includes(':')))
69
+ .filter(l => isValid(l.split(':')[0]))
70
+
71
+ // // filter out 3rd party libs
72
+ .filter(l => !l.includes('vendor' + sep))
73
+ .filter(l => !l.includes('node_modules' + sep))
74
+ .filter(l => fs.existsSync(l.split(':')[0]))
75
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile())
76
+
77
+ if (!stackLines.length) return '';
78
+
79
+ const [file, line] = stackLines[0].split(':');
80
+
81
+ const prepend = 3;
82
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
83
+
84
+ return source.split('\n')
85
+ .map((l, i) => {
86
+ if (i === prepend) return `${line} > ${chalk.bold(l)}`;
87
+ return `${line - prepend + i} | ${l}`
88
+ }).join('\n')
89
+ }
90
+
91
+ const fetchSourceCode = (contents, opts = {}) => {
92
+ if (!opts.title && !opts.line) return '';
93
+
94
+ // code fragment is 20 lines
95
+ const limit = opts.limit || 50;
96
+ let lineIndex;
97
+ if (opts.line) lineIndex = opts.line - 1;
98
+ const lines = contents.split('\n')
99
+
100
+ // remove special chars from title
101
+ if (!lineIndex && opts.title) {
102
+ const title = opts.title.replace(/[\(\[\@].*/g, '')
103
+ lineIndex = lines.findIndex(l => l.includes(title))
104
+ }
105
+
106
+ if (opts.prepend) {
107
+ lineIndex -= opts.prepend;
108
+ }
109
+
110
+ if (lineIndex) {
111
+ const result = [];
112
+ for (let i = lineIndex; i < (lineIndex + limit); i++) {
113
+ if (lines[i] === undefined) continue;
114
+
115
+ if (i > lineIndex + 2 && !opts.prepend) {
116
+ // annotation
117
+ if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
118
+ if (opts.lang === 'php' && lines[i].includes(' private function ')) break;
119
+ if (opts.lang === 'php' && lines[i].includes(' protected function ')) break;
120
+ if (opts.lang === 'php' && lines[i].includes(' public function ')) break;
121
+ if (opts.lang === 'python' && lines[i].trim().match(/^\@\w+/)) break;
122
+ if (opts.lang === 'python' && lines[i].includes(' def ')) break;
123
+ if (opts.lang === 'ruby' && lines[i].includes(' def ')) break;
124
+ if (opts.lang === 'ruby' && lines[i].includes(' test ')) break;
125
+ if (opts.lang === 'ruby' && lines[i].includes(' it ')) break;
126
+ if (opts.lang === 'ruby' && lines[i].includes(' specify ')) break;
127
+ if (opts.lang === 'ruby' && lines[i].includes(' context ')) break;
128
+ if (opts.lang === 'ts' && lines[i].includes(' it(')) break;
129
+ if (opts.lang === 'ts' && lines[i].includes(' test(')) break;
130
+ if (opts.lang === 'js' && lines[i].includes(' it(')) break;
131
+ if (opts.lang === 'js' && lines[i].includes(' test(')) break;
132
+ if (opts.lang === 'java' && lines[i].trim().match(/^\@\w+/)) break;
133
+ if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
134
+ if (opts.lang === 'java' && lines[i].includes(' class ')) break;
135
+ }
136
+ result.push(lines[i])
137
+ }
138
+ return result.join('\n');
139
+ }
140
+
141
+ }
142
+
51
143
  module.exports = {
52
144
  parseTest,
53
145
  parseSuite,
54
146
  ansiRegExp,
55
147
  isValidUrl,
148
+ fetchSourceCode,
149
+ fetchSourceCodeFromStackTrace,
150
+ fetchFilesFromStackTrace,
56
151
  };