@testomatio/reporter 1.2.2-beta → 1.2.4-beta

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.
@@ -1,9 +1,9 @@
1
1
  const debug = require('debug')('@testomatio/reporter:adapter:codeceptjs');
2
2
  const chalk = require('chalk');
3
3
  const TestomatClient = require('../client');
4
- const { STATUS, APP_PREFIX } = require('../constants');
4
+ const { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE } = require('../constants');
5
5
  const upload = require('../fileUploader');
6
- const Output = require('../output');
6
+ const { parseTest: getIdFromTestTitle, fileSystem } = require('../util');
7
7
 
8
8
  if (!global.codeceptjs) {
9
9
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
@@ -16,9 +16,9 @@ let currentMetaStep = [];
16
16
  let error;
17
17
  let stepShift = 0;
18
18
 
19
- const output = new Output({
20
- filterFn: stack => !stack.includes('codeceptjs/lib/output'), // output from codeceptjs
21
- });
19
+ // const output = new Output({
20
+ // filterFn: stack => !stack.includes('codeceptjs/lib/output'), // output from codeceptjs
21
+ // });
22
22
 
23
23
  let stepStart = new Date();
24
24
 
@@ -51,24 +51,34 @@ function CodeceptReporter(config) {
51
51
 
52
52
  recorder.startUnlessRunning();
53
53
 
54
+ global.testomatioRunningEnvironment = 'codeceptjs';
55
+
54
56
  // Listening to events
55
57
  event.dispatcher.on(event.all.before, () => {
58
+ // clear tmp dir
59
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
60
+
56
61
  recorder.add('Creating new run', () => client.createRun());
57
62
  videos = [];
58
63
  traces = [];
64
+
65
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
59
66
  });
60
67
 
61
68
  event.dispatcher.on(event.test.before, () => {
62
69
  recorder.add(() => {
63
70
  currentMetaStep = [];
64
- output.reset();
65
- output.start();
71
+ // output.reset();
72
+ // output.start();
66
73
  stepShift = 0;
67
74
  });
75
+
76
+ global.testomatioDataStore.steps = [];
68
77
  });
69
78
 
70
79
  event.dispatcher.on(event.test.started, test => {
71
80
  testTimeMap[test.id] = Date.now();
81
+ if (global.testomatioDataStore) global.testomatioDataStore.currentlyRunningTestId = getIdFromTestTitle(test.title);
72
82
  });
73
83
 
74
84
  event.dispatcher.on(event.all.result, async () => {
@@ -97,10 +107,10 @@ function CodeceptReporter(config) {
97
107
  suite_title: test.parent && test.parent.title,
98
108
  message: testObj.message,
99
109
  time: getDuration(test),
100
- steps: output.text(),
110
+ steps: global.testomatioDataStore.steps.join('\n') || null,
101
111
  test_id: testId,
102
112
  });
103
- output.stop();
113
+ // output.stop();
104
114
  });
105
115
 
106
116
  event.dispatcher.on(event.test.failed, (test, err) => {
@@ -125,7 +135,7 @@ function CodeceptReporter(config) {
125
135
  time: 0,
126
136
  });
127
137
  }
128
- output.stop();
138
+ // output.stop();
129
139
  });
130
140
 
131
141
  event.dispatcher.on(event.test.after, test => {
@@ -141,32 +151,33 @@ function CodeceptReporter(config) {
141
151
 
142
152
  const files = [];
143
153
  if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
144
- // todo: video must be uploaded later....
154
+ // todo: video must be uploaded later....
145
155
 
146
-
147
- const reportTestPromise = client.addTestRun(STATUS.FAILED, {
148
- ...stripExampleFromTitle(title),
149
- test_id: testId,
150
- suite_title: test.parent && test.parent.title,
151
- error,
152
- message: testObj.message,
153
- time: getDuration(test),
154
- files,
155
- steps: output.text(),
156
- }).then(pipes => {
157
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
158
-
159
- debug("artifacts", artifacts);
160
-
161
- for (const aid in artifacts) {
162
- if (aid.startsWith('video')) videos.push({ testId, title, path: artifacts[aid], type: 'video/webm' });
163
- if (aid.startsWith('trace')) traces.push({ testId, title, path: artifacts[aid], type: 'application/zip' });
164
- }
165
- });
156
+ const reportTestPromise = client
157
+ .addTestRun(STATUS.FAILED, {
158
+ ...stripExampleFromTitle(title),
159
+ test_id: testId,
160
+ suite_title: test.parent && test.parent.title,
161
+ error,
162
+ message: testObj.message,
163
+ time: getDuration(test),
164
+ files,
165
+ steps: global.testomatioDataStore.steps.join('\n') || null,
166
+ })
167
+ .then(pipes => {
168
+ testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
169
+
170
+ debug('artifacts', artifacts);
171
+
172
+ for (const aid in artifacts) {
173
+ if (aid.startsWith('video')) videos.push({ testId, title, path: artifacts[aid], type: 'video/webm' });
174
+ if (aid.startsWith('trace')) traces.push({ testId, title, path: artifacts[aid], type: 'application/zip' });
175
+ }
176
+ });
166
177
 
167
178
  reportTestPromises.push(reportTestPromise);
168
179
 
169
- output.stop();
180
+ // output.stop();
170
181
  });
171
182
 
172
183
  event.dispatcher.on(event.test.skipped, test => {
@@ -182,7 +193,7 @@ function CodeceptReporter(config) {
182
193
  message: testObj.message,
183
194
  time: getDuration(test),
184
195
  });
185
- output.stop();
196
+ // output.stop();
186
197
  });
187
198
 
188
199
  event.dispatcher.on(event.step.started, step => {
@@ -207,30 +218,37 @@ function CodeceptReporter(config) {
207
218
  // eslint-disable-next-line no-continue
208
219
  if (!metaSteps[i]) continue;
209
220
  if (metaSteps[i].isBDD()) {
210
- output.push(repeat(stepShift) + chalk.bold(metaSteps[i].toString()) + metaSteps[i].comment);
221
+ // output.push(repeat(stepShift) + chalk.bold(metaSteps[i].toString()) + metaSteps[i].comment);
222
+ global.testomatioDataStore.steps.push(
223
+ repeat(stepShift) + chalk.bold(metaSteps[i].toString()) + metaSteps[i].comment,
224
+ );
211
225
  } else {
212
- output.push(repeat(stepShift) + chalk.green.bold(metaSteps[i].toString()));
226
+ // output.push(repeat(stepShift) + chalk.green.bold(metaSteps[i].toString()));
227
+ global.testomatioDataStore.steps.push(repeat(stepShift) + chalk.green.bold(metaSteps[i].toString()));
213
228
  }
214
229
  }
215
230
  }
216
231
  currentMetaStep = metaSteps;
217
232
  stepShift = 2 * shift;
218
233
 
219
- const durationMs = (+new Date() - (+stepStart));
234
+ const durationMs = +new Date() - (+stepStart);
220
235
  let duration = '';
221
236
  if (durationMs) {
222
237
  duration = repeat(1) + chalk.grey(`(${durationMs}ms)`);
223
238
  }
224
239
 
225
240
  if (step.status === STATUS.FAILED) {
226
- output.push(repeat(stepShift) + chalk.red(step.toString()) + duration);
241
+ // output.push(repeat(stepShift) + chalk.red(step.toString()) + duration);
242
+ global.testomatioDataStore.steps.push(repeat(stepShift) + chalk.red(step.toString()) + duration);
227
243
  } else {
228
- output.push(repeat(stepShift) + step.toString() + duration);
244
+ // output.push(repeat(stepShift) + step.toString() + duration);
245
+ global.testomatioDataStore.steps.push(repeat(stepShift) + step.toString() + duration);
229
246
  }
230
247
  });
231
248
 
232
249
  event.dispatcher.on(event.step.comment, step => {
233
- output.push(chalk.cyan.bold(step.toString()));
250
+ // output.push(chalk.cyan.bold(step.toString()));
251
+ global.testomatioDataStore.steps.push(chalk.cyan.bold(step.toString()));
234
252
  });
235
253
  }
236
254
 
@@ -1,35 +1,59 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
- const { Formatter, formatterHelpers } = require("@cucumber/cucumber");
2
+ const { Formatter, formatterHelpers } = require('@cucumber/cucumber');
3
3
  const chalk = require('chalk');
4
4
  const fs = require('fs');
5
- const { STATUS } = require('../../constants');
5
+ const { STATUS, TESTOMAT_TMP_STORAGE } = require('../../constants');
6
6
  const TestomatClient = require('../../client');
7
+ const logger = require('../../logger');
8
+ const { parseTest, fileSystem } = require('../../util');
9
+
10
+ const { GherkinDocumentParser, PickleParser } = formatterHelpers;
11
+ const { getGherkinScenarioLocationMap, getGherkinStepMap } = GherkinDocumentParser;
12
+ const { getPickleStepMap } = PickleParser;
13
+
14
+ function getTestId(scenario) {
15
+ if (scenario) {
16
+ for (const tag of scenario.tags) {
17
+ const testId = parseTest(tag.name);
18
+ if (testId) return testId;
19
+ }
20
+ }
7
21
 
8
- const { GherkinDocumentParser, PickleParser } = formatterHelpers
9
- const {
10
- getGherkinScenarioLocationMap,
11
- getGherkinStepMap,
12
- } = GherkinDocumentParser
13
- const { getPickleStepMap } = PickleParser
22
+ return null;
23
+ }
14
24
 
15
25
  class CucumberReporter extends Formatter {
16
26
  constructor(options) {
17
27
  super(options);
18
- options.eventBroadcaster.on('envelope', this.parseEnvelope.bind(this))
28
+ options.eventBroadcaster.on('envelope', this.parseEnvelope.bind(this));
19
29
  this.failures = [];
20
30
  this.cases = [];
21
31
 
22
32
  this.client = new TestomatClient();
23
33
  this.status = STATUS.PASSED;
24
34
 
35
+ global.testomatioRunningEnvironment = 'cucumber:current';
25
36
  }
26
37
 
27
38
  parseEnvelope(envelope) {
28
- if (envelope.testCaseStarted && this.client) this.client.createRun()
39
+ if (envelope.testRunStarted) {
40
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
41
+ }
42
+ if (envelope.testCaseStarted && this.client) {
43
+ this.client.createRun();
44
+ this.onTestCaseStarted(envelope.testCaseStarted);
45
+ }
29
46
  if (envelope.testCaseFinished) this.onTestCaseFinished(envelope.testCaseFinished);
30
47
  if (envelope.testRunFinished) this.onTestRunFinished(envelope);
31
48
  }
32
49
 
50
+ onTestCaseStarted(testCaseStarted) {
51
+ const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(testCaseStarted.id);
52
+ const testId = getTestId(testCaseAttempt.pickle);
53
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
54
+ global.testomatioDataStore.currentlyRunningTestId = testId;
55
+ }
56
+
33
57
  onTestCaseFinished(testCaseFinished) {
34
58
  const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(testCaseFinished.testCaseStartedId);
35
59
 
@@ -45,10 +69,10 @@ class CucumberReporter extends Formatter {
45
69
  if (testCaseAttempt.worstTestStepResult.status === 'SKIPPED') {
46
70
  status = STATUS.SKIPPED;
47
71
  }
48
- if (testCaseAttempt.worstTestStepResult.status === 'UNDEFINED') {
49
- status = STATUS.SKIPPED;
50
- message = 'Undefined steps. Implement missing steps and rerun this scenario';
51
- }
72
+ // if (testCaseAttempt.worstTestStepResult.status === 'UNDEFINED') {
73
+ // status = STATUS.SKIPPED;
74
+ // message = 'Undefined steps. Implement missing steps and rerun this scenario';
75
+ // }
52
76
  if (testCaseAttempt.worstTestStepResult.status === 'FAILED') {
53
77
  message = testCaseAttempt?.worstTestStepResult?.message;
54
78
  status = STATUS.FAILED;
@@ -64,10 +88,12 @@ class CucumberReporter extends Formatter {
64
88
  }
65
89
 
66
90
  const scenario = testCaseAttempt.pickle.name;
67
- const testId = testCaseAttempt.pickle.id;
91
+ // this may broke something (it is supposed to work, but I am sure it did not)
92
+ // const testId = testCaseAttempt.pickle.id;
93
+ const testId = getTestId(testCaseAttempt.pickle);
68
94
 
69
95
  let exampleString = '';
70
- if (example) exampleString = ` ${example.join(' | ')}`
96
+ if (example) exampleString = ` ${example.join(' | ')}`;
71
97
  let cliMessage = `${chalk.bold(scenario)}${exampleString}: ${chalk[color].bold(status.toUpperCase())} `;
72
98
 
73
99
  if (message) cliMessage += chalk.gray(message.split('\n')[0]);
@@ -79,15 +105,21 @@ class CucumberReporter extends Formatter {
79
105
 
80
106
  const time = Object.values(testCaseAttempt.stepResults)
81
107
  .map(t => t.duration)
82
- .reduce((sum, duration) => sum + (duration.seconds * 1000) + (duration.nanos / 1000000), 0)
108
+ .reduce((sum, duration) => sum + duration.seconds * 1000 + duration.nanos / 1000000, 0);
83
109
 
84
110
  if (!this.client) return;
85
111
 
112
+ const logs = logger.getLogs(testId);
113
+
86
114
  this.client.addTestRun(status, {
87
115
  // error: testCaseAttempt.worstTestStepResult.message,
88
116
  message,
89
- steps: getSteps(testCaseAttempt).map(s => s.toString()).join('\n').trim(),
90
- example: { ...example},
117
+ steps: getSteps(testCaseAttempt)
118
+ .map(s => s.toString())
119
+ .join('\n')
120
+ .trim(),
121
+ example: { ...example },
122
+ stack: logs,
91
123
  title: scenario,
92
124
  test_id: testId,
93
125
  time,
@@ -99,21 +131,20 @@ class CucumberReporter extends Formatter {
99
131
  console.log(chalk.bold('\nSUMMARY:\n\n'));
100
132
 
101
133
  this.failures.forEach((tc, i) => {
102
- let message = ` ${i+1}) ${tc.pickle.name}\n`;
134
+ let message = ` ${i + 1}) ${tc.pickle.name}\n`;
103
135
 
104
136
  const steps = getSteps(tc);
105
137
 
106
138
  steps.forEach(s => {
107
- message += ` ${s.toString()}\n`
108
- })
139
+ message += ` ${s.toString()}\n`;
140
+ });
109
141
 
110
142
  console.log(message);
111
143
  if (tc?.worstTestStepResult?.message) {
112
144
  console.log(chalk.red(tc?.worstTestStepResult?.message));
113
145
  }
114
146
  console.log();
115
-
116
- })
147
+ });
117
148
  }
118
149
 
119
150
  const { testRunFinished } = envelope;
@@ -125,46 +156,48 @@ class CucumberReporter extends Formatter {
125
156
 
126
157
  if (!this.client) return;
127
158
 
128
- this.client.updateRunStatus(testRunFinished.success ? STATUS.PASSED : STATUS.FAILED)
159
+ this.client.updateRunStatus(testRunFinished.success ? STATUS.PASSED : STATUS.FAILED);
129
160
  }
130
161
  }
131
162
 
132
163
  function getSteps(tc) {
133
164
  const stepIds = Object.keys(tc.stepResults);
134
- const pickleSteps = getPickleStepMap(tc.pickle)
135
- return stepIds.map(stepId => {
136
- const ts = tc.testCase.testSteps.find(t => t.id === stepId);
137
- if (!ts) return;
138
- if (!ts.pickleStepId) return;
139
- const result = tc.stepResults[stepId];
140
- const pickleStep = pickleSteps[ts.pickleStepId];
141
- const sourceStepId = pickleStep.astNodeIds[0];
142
- const step = {
143
- text: pickleStep.text,
144
- duration: result.duration,
145
- status: result.status,
146
- }
147
- const color = getStatusColor(result.status);
148
- if (sourceStepId && getGherkinStepMap(tc.gherkinDocument)[sourceStepId]) {
149
- step.keyword = getGherkinStepMap(tc.gherkinDocument)[sourceStepId].keyword;
150
- }
151
- step.toString = function toString() {
152
- const duration = step.duration.seconds * 1000 + (step.duration.nanos / 1000000)
153
- const durationString = ` ${ chalk.gray(`(${Number(duration).toFixed(2)}ms)`)}`;
154
- const stepString = `${chalk.bold(this.keyword)}${this.text}`.trim();
155
- if (color === 'red') return chalk.red(stepString) + durationString;
156
- if (color === 'yellow') return chalk.yellow(stepString) + durationString;
157
- return stepString + durationString;
158
- }
159
- return step;
160
- }).filter(s => !!s)
165
+ const pickleSteps = getPickleStepMap(tc.pickle);
166
+ return stepIds
167
+ .map(stepId => {
168
+ const ts = tc.testCase.testSteps.find(t => t.id === stepId);
169
+ if (!ts) return;
170
+ if (!ts.pickleStepId) return;
171
+ const result = tc.stepResults[stepId];
172
+ const pickleStep = pickleSteps[ts.pickleStepId];
173
+ const sourceStepId = pickleStep.astNodeIds[0];
174
+ const step = {
175
+ text: pickleStep.text,
176
+ duration: result.duration,
177
+ status: result.status,
178
+ };
179
+ const color = getStatusColor(result.status);
180
+ if (sourceStepId && getGherkinStepMap(tc.gherkinDocument)[sourceStepId]) {
181
+ step.keyword = getGherkinStepMap(tc.gherkinDocument)[sourceStepId].keyword;
182
+ }
183
+ step.toString = function toString() {
184
+ const duration = step.duration.seconds * 1000 + step.duration.nanos / 1000000;
185
+ const durationString = ` ${chalk.gray(`(${Number(duration).toFixed(2)}ms)`)}`;
186
+ const stepString = `${chalk.bold(this.keyword)}${this.text}`.trim();
187
+ if (color === 'red') return chalk.red(stepString) + durationString;
188
+ if (color === 'yellow') return chalk.yellow(stepString) + durationString;
189
+ return stepString + durationString;
190
+ };
191
+ return step;
192
+ })
193
+ .filter(s => !!s);
161
194
  }
162
195
 
163
196
  function getStatusColor(status) {
164
197
  if (status === 'UNDEFINED') return 'yellow';
165
198
  if (status === 'SKIPPED') return 'yellow';
166
199
  if (status === 'FAILED') return 'red';
167
- return 'green'
200
+ return 'green';
168
201
  }
169
202
 
170
203
  function getExample(testCaseAttempt) {
@@ -173,11 +206,14 @@ function getExample(testCaseAttempt) {
173
206
  if (!nodesMap[exampleNodeId]) return;
174
207
  const featureDoc = fs.readFileSync(testCaseAttempt.gherkinDocument.uri).toString();
175
208
  const { line } = nodesMap[exampleNodeId];
176
- const example = featureDoc.split('\n')[line-1];
209
+ const example = featureDoc.split('\n')[line - 1];
177
210
  if (example) {
178
- return example.trim().split('|').filter(r => !!r).map(r => r.trim());
179
- };
180
-
211
+ return example
212
+ .trim()
213
+ .split('|')
214
+ .filter(r => !!r)
215
+ .map(r => r.trim());
216
+ }
181
217
  }
182
218
 
183
219
  module.exports = CucumberReporter;
@@ -1,8 +1,8 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
2
2
  const { Formatter } = require('cucumber');
3
3
  const chalk = require('chalk');
4
- const { parseTest } = require('../../util');
5
- const { STATUS } = require('../../constants');
4
+ const { parseTest, fileSystem } = require('../../util');
5
+ const { STATUS, TESTOMAT_TMP_STORAGE } = require('../../constants');
6
6
  const TestomatClient = require('../../client');
7
7
 
8
8
  const createTestomatFormatter = apiKey => {
@@ -96,9 +96,23 @@ const createTestomatFormatter = apiKey => {
96
96
  this.status = STATUS.PASSED.toString();
97
97
 
98
98
  options.eventBroadcaster.on('gherkin-document', addDocument);
99
- options.eventBroadcaster.on('test-run-started', () => this?.client?.createRun());
99
+ options.eventBroadcaster.on('test-run-started', () => {
100
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
101
+ this?.client?.createRun();
102
+ });
100
103
  options.eventBroadcaster.on('test-case-finished', this.onTestCaseFinished.bind(this));
104
+ options.eventBroadcaster.on('test-case-started', this.onTestCaseStarted.bind(this));
101
105
  options.eventBroadcaster.on('test-run-finished', () => this?.client?.updateRunStatus(this.status));
106
+
107
+ global.testomatioRunningEnvironment = 'cucumber:legacy';
108
+ }
109
+
110
+ onTestCaseStarted(event) {
111
+ const scenario = getScenario(event.sourceLocation);
112
+ const testId = getTestId(scenario);
113
+
114
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
115
+ global.testomatioDataStore.currentlyRunningTestId = testId;
102
116
  }
103
117
 
104
118
  onTestCaseFinished(event) {
@@ -114,12 +128,12 @@ const createTestomatFormatter = apiKey => {
114
128
  let message = '';
115
129
  let cliMessage = `- ${scenario.name}: ${chalk.bold(status.toUpperCase())}`;
116
130
 
117
- if (event.result.status === 'undefined') {
118
- cliMessage += chalk.yellow(
119
- ' (undefined steps. Run Cucumber without this formatter and implement missing steps)',
120
- );
121
- message = 'Undefined steps. Implement missing steps and rerun this scenario';
122
- }
131
+ // if (event.result.status === 'undefined') {
132
+ // cliMessage += chalk.yellow(
133
+ // ' (undefined steps. Run Cucumber without this formatter and implement missing steps)',
134
+ // );
135
+ // message = 'Undefined steps. Implement missing steps and rerun this scenario';
136
+ // }
123
137
  console.log(cliMessage);
124
138
  if (status !== STATUS.PASSED && status !== STATUS.SKIPPED) {
125
139
  this.status = STATUS.FAILED;
@@ -13,8 +13,13 @@ class JestReporter {
13
13
 
14
14
  static getIdOfCurrentlyRunningTest() {
15
15
  if (!process.env.JEST_WORKER_ID) return null;
16
- // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
17
- return parseTest(expect?.getState()?.currentTestName) || null; // eslint-disable-line
16
+ try {
17
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
18
+ // eslint-disable-next-line no-undef
19
+ if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
20
+ } catch (e) {
21
+ return null;
22
+ }
18
23
  }
19
24
 
20
25
  onRunStart() {
@@ -3,10 +3,11 @@ const crypto = require('crypto');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const { APP_PREFIX, STATUS: Status } = require('../constants');
6
+ const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE } = require('../constants');
7
7
  const TestomatioClient = require('../client');
8
8
  const { isArtifactsEnabled } = require('../fileUploader');
9
- const { parseTest } = require('../util');
9
+ const { parseTest, fileSystem } = require('../util');
10
+ const { setCurrentlyRunningTestId } = require('../helpers');
10
11
 
11
12
  const reportTestPromises = [];
12
13
 
@@ -21,9 +22,21 @@ class TestomatioReporter {
21
22
  if (!this.client) return;
22
23
  this.suite = suite;
23
24
  this.client.createRun();
25
+
26
+ // for data storage
27
+ // fileSystem.createDir(TESTOMAT_TMP_STORAGE.mainDir);
28
+ }
29
+
30
+ onTestBegin(test, result) {
31
+ const testId = parseTest(test.title);
32
+ setCurrentlyRunningTestId(testId);
33
+ // global.testomatioDataStore.currentlyRunningTestId = testId;
34
+ // TestomatioReporter.currentlyRunningTestId = testId;
24
35
  }
25
36
 
26
37
  onTestEnd(test, result) {
38
+ // reset currently running test id
39
+ // setCurrentlyRunningTestId('');
27
40
  if (!this.client) return;
28
41
 
29
42
  let testId = parseTest(test.title);
@@ -37,21 +50,26 @@ class TestomatioReporter {
37
50
  for (const step of result.steps) {
38
51
  appendStep(step, steps);
39
52
  }
40
-
41
- const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
42
- error,
43
- test_id: testId,
44
- suite_title,
45
- title,
46
- steps: steps.join('\n'),
47
- time: duration,
48
- }).then(pipes => {
49
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
50
-
51
- this.uploads.push({
52
- testId, title, suite_title, files: result.attachments.filter((a) => a.body || a.path)
53
+
54
+ const reportTestPromise = this.client
55
+ .addTestRun(checkStatus(result.status), {
56
+ error,
57
+ test_id: testId,
58
+ suite_title,
59
+ title,
60
+ steps: steps.join('\n'),
61
+ time: duration,
53
62
  })
54
- });;
63
+ .then(pipes => {
64
+ testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
65
+
66
+ this.uploads.push({
67
+ testId,
68
+ title,
69
+ suite_title,
70
+ files: result.attachments.filter(a => a.body || a.path),
71
+ });
72
+ });
55
73
 
56
74
  reportTestPromises.push(reportTestPromise);
57
75
  }
@@ -67,10 +85,9 @@ class TestomatioReporter {
67
85
  const promises = [];
68
86
 
69
87
  for (const upload of this.uploads) {
70
-
71
88
  const { title, testId, suite_title } = upload;
72
89
 
73
- const files = upload.files.map((attachment) => {
90
+ const files = upload.files.map(attachment => {
74
91
  if (attachment.body) {
75
92
  const fileName = tmpFile();
76
93
  fs.writeFileSync(fileName, attachment.body);
@@ -78,7 +95,6 @@ class TestomatioReporter {
78
95
  return { path: attachment.path, title, type: attachment.contentType };
79
96
  });
80
97
 
81
-
82
98
  promises.push(
83
99
  this.client.addTestRun(undefined, {
84
100
  test_id: testId,
package/lib/client.js CHANGED
@@ -7,7 +7,6 @@ const { randomUUID } = require('crypto');
7
7
  const upload = require('./fileUploader');
8
8
  const { APP_PREFIX } = require('./constants');
9
9
  const pipesFactory = require('./pipe');
10
- const logger = require('./logger');
11
10
 
12
11
  /**
13
12
  * @typedef {import('../types').TestData} TestData
@@ -48,13 +47,13 @@ class Client {
48
47
  };
49
48
 
50
49
  global.testomatioArtifacts = [];
51
- // TODO: create global storage
50
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
52
51
 
53
52
  this.queue = this.queue
54
53
  .then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))))
55
54
  .catch(err => console.log(APP_PREFIX, err))
56
55
  .then(() => undefined); // fixes return type
57
- debug('Run', this.queue);
56
+ // debug('Run', this.queue);
58
57
  return this.queue;
59
58
  }
60
59
 
@@ -93,20 +92,22 @@ class Client {
93
92
 
94
93
  const uploadedFiles = [];
95
94
 
96
- let stack = '';
95
+ let stack = testData.stack || '';
97
96
 
98
97
  if (error) {
99
98
  stack = this.formatError(error) || '';
100
99
  message = error?.message;
101
100
  }
102
101
  if (steps) {
103
- stack = this.formatSteps(stack, steps);
102
+ stack += this.formatSteps(stack, steps);
104
103
  }
105
104
 
105
+ // in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
106
+ const logger = require('./logger');
106
107
  const testLogs = logger.getLogs(test_id);
107
- debug(`Test logs for ${test_id}:\n`, testLogs);
108
+ // debug(`Test logs for ${test_id}:\n`, testLogs);
108
109
  if (stack) stack += '\n\n';
109
- stack += testLogs;
110
+ stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
110
111
 
111
112
  if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
112
113
  debug('CLIENT storeArtifact', storeArtifacts);
@@ -6,21 +6,41 @@ const { join } = require('path');
6
6
  const JestReporter = require('./adapter/jest');
7
7
  const { TESTOMAT_TMP_STORAGE } = require('./constants');
8
8
  const { fileSystem } = require('./util');
9
-
9
+ // const { getCurrentlyRunningTestId } = require('./helpers');
10
10
  class DataStorage {
11
11
  /**
12
12
  * Creates data storage instance for specific data type.
13
13
  * Stores data to global variable or to file depending on what is applicable for current test runner.
14
14
  * dataType: 'log' | 'artifact' | ...
15
15
  * @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
16
- * @param {*} params
17
16
  */
18
- constructor(dataType, params = {}) {
19
- if (!dataType) throw new Error('Data type is required when creating data storage');
17
+ constructor(dataType) {
18
+ // if (!dataType) throw new Error('Data type is required when creating data storage');
20
19
  this.dataType = dataType || 'data';
21
- this.dataDirName = `${dataType}s`;
22
- this.isFileStorage = params?.isFileStorage ?? !global.testomatioDataStore;
20
+ this.dataDirName = this.dataType;
21
+
22
+ this._refreshStorageType();
23
+
24
+ // dir is created in any case (not to recheck its existence every time)
25
+ this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataDirName);
26
+ fileSystem.createDir(this.dataDirPath);
27
+
28
+ debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
29
+ }
30
+
31
+ // TODO: implement
32
+ // eslint-disable-next-line no-unused-vars
33
+ getTestIdFromContext(context) {
34
+ // return testId;
35
+ }
23
36
 
37
+ /**
38
+ * Refreshes storage type (file or global variable) depending on current environment
39
+ * This method should be run on each attempt to put or get data from/to storage
40
+ * Because storage instance is created before the test runner is started
41
+ */
42
+ _refreshStorageType() {
43
+ this.isFileStorage = !global.testomatioDataStore;
24
44
  /*
25
45
  FYI:
26
46
  If this storage instance is used within test runner, it works fine.
@@ -30,20 +50,10 @@ class DataStorage {
30
50
  and potentially useless when get data from storage
31
51
  */
32
52
  this.runningEnvironment = this.getRunningEnviroment();
33
- if (this.runningEnvironment === 'jest') this.isFileStorage = true;
34
-
35
- if (this.isFileStorage) {
36
- this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataDirName);
37
- fileSystem.createDir(this.dataDirPath);
38
- }
39
53
 
40
- debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
41
- }
42
-
43
- // TODO: implement
44
- getTestIdFromContext(context) { // eslint-disable-line
45
- // eslint-disable-line
46
- // return testId;
54
+ // jest does not persist global variables, thus file storage is used
55
+ if (this.runningEnvironment === 'jest') this.isFileStorage = true;
56
+ if (this.runningEnvironment === 'playwright') this.isFileStorage = true;
47
57
  }
48
58
 
49
59
  /**
@@ -51,14 +61,28 @@ class DataStorage {
51
61
  * @returns jest | mocha | ...
52
62
  */
53
63
  getRunningEnviroment() {
64
+ // jest
54
65
  if (process.env.JEST_WORKER_ID) return 'jest';
66
+
67
+ // mocha
55
68
  try {
56
69
  // @ts-expect-error mocha is defined only in mocha environment
57
70
  if (typeof mocha !== 'undefined') return 'mocha';
58
71
  } catch (e) {
59
72
  // ignore
60
73
  }
61
- return undefined;
74
+
75
+ // codeceptjs
76
+ // @ts-expect-error codeceptjs is defined only in codeceptjs environment
77
+ if (global.codeceptjs) return 'codeceptjs';
78
+
79
+ // others
80
+ // 'cucumber:current', 'cucumber:legacy'
81
+ if (global.testomatioRunningEnvironment) return global.testomatioRunningEnvironment;
82
+
83
+ if (process.env.PLAYWRIGHT_TEST_BASE_URL) return 'playwright';
84
+
85
+ return null;
62
86
  }
63
87
 
64
88
  /**
@@ -68,7 +92,7 @@ class DataStorage {
68
92
  * @returns
69
93
  */
70
94
  putData(data, context = null) {
71
- // this.isFileStorage = !global.testomatioDataStore;
95
+ this._refreshStorageType();
72
96
 
73
97
  let testId = null;
74
98
  if (typeof context === 'string') {
@@ -80,9 +104,17 @@ class DataStorage {
80
104
 
81
105
  // try to get testId for Jest
82
106
  if (this.runningEnvironment === 'jest') testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
107
+ if (this.runningEnvironment === 'codeceptjs') testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
108
+ // if (this.runningEnvironment === 'playwright') testId = testId ?? getCurrentlyRunningTestId();
109
+ // get id from global store
110
+ if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
111
+ testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
83
112
 
84
113
  // if testId is not provided, data is be saved to `{dataType}_other` file;
85
- if (!testId) testId = 'other';
114
+ if (!testId) {
115
+ debug(`No test id provided for ${this.dataType} data: ${data}`);
116
+ return;
117
+ }
86
118
 
87
119
  if (this.isFileStorage) {
88
120
  this._putDataToFile(data, testId);
@@ -92,13 +124,17 @@ class DataStorage {
92
124
  }
93
125
 
94
126
  /**
95
- * Returns data, stored for specific testId (or data which was stored without test id specified)
127
+ * Returns data, stored for specific testId (or data which was stored without test id specified).
128
+ * This method will get data from global variable. But if it is not available, it will try to get data from file.
129
+ * Thus, good approach is to remove file storage folder before each test run (and after, for sure).
96
130
  *
97
- * (Don't try to guess the execution environment (e.g. test runner) inside this method, it could be any)
131
+ * Defining the execution environment is not guaranteed! Is used only as additional check.
98
132
  * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
99
133
  * @returns
100
134
  */
101
135
  getData(context) {
136
+ this._refreshStorageType();
137
+
102
138
  let testId = null;
103
139
  if (typeof context === 'string') {
104
140
  testId = context;
@@ -109,16 +145,23 @@ class DataStorage {
109
145
 
110
146
  if (!testId) {
111
147
  debug(`Cannot get test id from passed context:\n}`, context);
148
+ return null;
112
149
  }
113
150
 
114
- if (this.isFileStorage) {
115
- return this._getDataFromFile(testId);
116
- }
151
+ let testData = null;
152
+
117
153
  if (global?.testomatioDataStore) {
118
- return this._getDataFromGlobalVar(testId);
154
+ testData = this._getDataFromGlobalVar(testId);
155
+ if (testData) return testData;
119
156
  }
157
+
158
+ if (this.isFileStorage || !testData) {
159
+ testData = this._getDataFromFile(testId);
160
+ if (testData) return testData;
161
+ }
162
+
120
163
  debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
121
- return null;
164
+ return testData;
122
165
  }
123
166
 
124
167
  _getDataFromGlobalVar(testId) {
@@ -128,7 +171,7 @@ class DataStorage {
128
171
  debug(`Data for test id ${testId}:\n${testData}`);
129
172
  return testData;
130
173
  }
131
- debug(`No ${this.dataType} data for test id ${testId}`);
174
+ debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
132
175
  return null;
133
176
  } catch (e) {
134
177
  // there could be no data, ignore
@@ -137,13 +180,13 @@ class DataStorage {
137
180
 
138
181
  _getDataFromFile(testId) {
139
182
  try {
140
- const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, `${this.dataType}_${testId}`);
183
+ const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
141
184
  if (fs.existsSync(filepath)) {
142
185
  const testData = fs.readFileSync(filepath, 'utf-8');
143
186
  debug(`Data for test id ${testId}:\n${testData}`);
144
187
  return testData;
145
188
  }
146
- debug(`No ${this.dataType} data for test id ${testId}`);
189
+ debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
147
190
  return null;
148
191
  } catch (e) {
149
192
  // there could be no data, ignore
@@ -153,14 +196,16 @@ class DataStorage {
153
196
 
154
197
  _putDataToGlobalVar(data, testId) {
155
198
  debug('Saving data to global variable for test', testId, ':\n', data, '\n');
156
- global.testomatioDataStore[this.dataDirName] = {};
157
- global.testomatioDataStore[this.dataDirName][testId] = data;
199
+ if (!global.testomatioDataStore[this.dataDirName]) global.testomatioDataStore[this.dataDirName] = {};
200
+ global.testomatioDataStore[this.dataDirName][testId] // eslint-disable-line no-unused-expressions
201
+ ? (global.testomatioDataStore[this.dataDirName][testId] += `\n${data}`)
202
+ : (global.testomatioDataStore[this.dataDirName][testId] = data);
158
203
  }
159
204
 
160
205
  _putDataToFile(data, testId) {
161
206
  if (typeof data !== 'string') data = JSON.stringify(data);
162
207
  const filename = `${this.dataType}_${testId}`;
163
- const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, filename);
208
+ const filepath = join(this.dataDirPath, filename);
164
209
  debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
165
210
 
166
211
  // TODO: handle multiple invocations of JSON.stringify.
@@ -177,4 +222,20 @@ module.exports.DataStorage = DataStorage;
177
222
  // TODO: rewrite client - everything regarding storing artifacts
178
223
  // TODO: try to define adapter inside client
179
224
  // TODO: use .env
180
- // TODO: ability to intercept multiple loggers
225
+ // TODO: ability to intercept multiple loggers (upd: no need)
226
+
227
+ /* Playwright
228
+ There is no global scope sharing between tests and reporter listeners.
229
+ Thus need to use file storage. But when tests are run in parallel, the new problem occurs.
230
+ Tried to solve it by using the TEST_WORKER_INDEX env variable, but it is available inside the listener
231
+ (available only inside the test). Thus when cannot match currently running test id from the reporter listener
232
+ with the currently running test.
233
+ The only solution I see for now is to pass the test id to the logger from the test itself.
234
+ By passing the testInfo param to the test function:
235
+ async ({ page }, testInfo)
236
+ and then retrieving the .title from it.
237
+ */
238
+
239
+ /* Cypress
240
+ Parallelization is only available if using Cypress Dashboard??
241
+ */
package/lib/helpers.js ADDED
@@ -0,0 +1,34 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { TESTOMAT_TMP_STORAGE } = require('./constants');
4
+
5
+ // NEXT HELPERS ARE CREATED FOR PLAYWRIGHT. IGNORE FOR NOW. PROBABLY WILL BE REMOVED SHORTLY
6
+
7
+ // TEST_WORKER_INDEX is not available inside the test listener :/
8
+ /**
9
+ * Sets id of currently running test to storage (global variable or file)
10
+ * Used for Playwright
11
+ * @param {*} testId
12
+ */
13
+ function setCurrentlyRunningTestIdForPlaywright(testId) {
14
+ const workerId = +process.env.TEST_WORKER_INDEX || 0;
15
+ const filePath = path.join(TESTOMAT_TMP_STORAGE.mainDir, 'current_test_id_' + workerId);
16
+ fs.writeFileSync(filePath, testId);
17
+ }
18
+
19
+ /**
20
+ * Gets id of currently running test from storage (global variable or file)
21
+ * Used for Playwright
22
+ * @returns string test id of currently running test
23
+ */
24
+ function getCurrentlyRunningTestIdForPlaywright() {
25
+ const workerId = +process.env.TEST_WORKER_INDEX || 0;
26
+ const filePath = path.join(TESTOMAT_TMP_STORAGE.mainDir, 'current_test_id_' + workerId);
27
+ if (fs.existsSync(filePath)) {
28
+ return fs.readFileSync(filePath, 'utf8') || null;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ module.exports.setCurrentlyRunningTestId = setCurrentlyRunningTestIdForPlaywright;
34
+ module.exports.getCurrentlyRunningTestId = getCurrentlyRunningTestIdForPlaywright;
package/lib/logger.js CHANGED
@@ -24,11 +24,11 @@ class Logger {
24
24
  // _originalUserLogger used to output logs to console by the user logger
25
25
  // _loggerToIntercept intercepted and reassigned immediately when added
26
26
 
27
- constructor(params = {}) {
27
+ constructor() {
28
28
  // set default logger to be used in log, warn, error, etc methods
29
29
  this._originalUserLogger = { ...console };
30
30
 
31
- this.dataStorage = new DataStorage('log', params);
31
+ this.dataStorage = new DataStorage('log');
32
32
  this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
33
33
 
34
34
  // commented because prefer to use "intercept" method
@@ -77,8 +77,8 @@ class Logger {
77
77
  /**
78
78
  * Allows you to define a step inside a test. Step name is attached to the report and
79
79
  * helps to understand the test flow.
80
- * @param {*} strings
81
- * @param {...any} values
80
+ * @param {*} strings
81
+ * @param {...any} values
82
82
  */
83
83
  step(strings, ...values) {
84
84
  let logs = '';
@@ -98,7 +98,8 @@ class Logger {
98
98
  * @returns
99
99
  */
100
100
  getLogs(context) {
101
- return this.dataStorage.getData(context);
101
+ const logs = this.dataStorage.getData(context);
102
+ return logs || '';
102
103
  }
103
104
 
104
105
  _strinfifyLogs(...args) {
@@ -121,6 +122,9 @@ class Logger {
121
122
  }
122
123
 
123
124
  assert(...args) {
125
+ // sometimes user invokes logger without any arguments passed
126
+ if (!args.length) return;
127
+
124
128
  const level = 'ERROR';
125
129
  const severity = LEVELS[level].severity;
126
130
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -135,6 +139,8 @@ class Logger {
135
139
  }
136
140
 
137
141
  debug(...args) {
142
+ if (!args.length) return;
143
+
138
144
  const level = 'DEBUG';
139
145
  const severity = LEVELS[level].severity;
140
146
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -151,6 +157,8 @@ class Logger {
151
157
  }
152
158
 
153
159
  error(...args) {
160
+ if (!args.length) return;
161
+
154
162
  const level = 'ERROR';
155
163
  const severity = LEVELS[level].severity;
156
164
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -166,6 +174,8 @@ class Logger {
166
174
  }
167
175
 
168
176
  info(...args) {
177
+ if (!args.length) return;
178
+
169
179
  const level = 'INFO';
170
180
  const severity = LEVELS[level].severity;
171
181
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -180,6 +190,8 @@ class Logger {
180
190
  }
181
191
 
182
192
  log(...args) {
193
+ if (!args.length) return;
194
+
183
195
  const level = 'INFO';
184
196
  const severity = LEVELS[level].severity;
185
197
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -194,6 +206,8 @@ class Logger {
194
206
  }
195
207
 
196
208
  trace(...args) {
209
+ if (!args.length) return;
210
+
197
211
  const level = 'TRACE';
198
212
  const severity = LEVELS[level].severity;
199
213
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -209,6 +223,8 @@ class Logger {
209
223
  }
210
224
 
211
225
  warn(...args) {
226
+ if (!args.length) return;
227
+
212
228
  const level = 'WARN';
213
229
  const severity = LEVELS[level].severity;
214
230
  if (severity < LEVELS[this.logLevel]?.severity) return;
@@ -233,7 +249,7 @@ class Logger {
233
249
  */
234
250
  intercept(userLogger) {
235
251
  if (!userLogger) return;
236
- debug(`Intercepting user logger`);
252
+ debug(`User logger intercepted`);
237
253
 
238
254
  // save original user logger to use it for logging (last intercepted will be used for console output)
239
255
  this._originalUserLogger = { ...userLogger };
@@ -275,4 +291,3 @@ module.exports = new Logger();
275
291
 
276
292
  // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
277
293
  // upd: did not face such loggers, but still could be useful
278
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.2.2-beta",
3
+ "version": "1.2.4-beta",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -51,7 +51,6 @@
51
51
  "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js"
52
52
  },
53
53
  "devDependencies": {
54
- "@cucumber/cucumber": "^8.6.0",
55
54
  "@redocly/cli": "^1.0.0-beta.125",
56
55
  "@wdio/reporter": "^7.16.13",
57
56
  "chai": "^4.3.6",