@testomatio/reporter 1.0.0 β†’ 1.0.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/README.md CHANGED
@@ -16,7 +16,8 @@ Testomat.io Reporter (this npm package) supports:
16
16
  * πŸš… Realtime reports
17
17
  * πŸ—ƒοΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
18
18
  * πŸšΆβ€β™€οΈ Steps *(work in progress)*
19
- * ☁️ Custom properties and metadata *(work in progress)*
19
+ * πŸ“„ Logger *(work in progress, supports Jest for now)*
20
+ * ☁️ Custom properties and metadata *(work in progress)*
20
21
  * πŸ’― Free & open-source.
21
22
  * πŸ“Š Public and private Run reports on cloud via [Testomat.io App](https://testomat.io) πŸ‘‡
22
23
 
@@ -64,7 +65,7 @@ yarn add @testomatio/reporter --dev
64
65
 
65
66
  ### 1️⃣ Attach Reporter to the Test Runner
66
67
 
67
- * #### [Playwright](./docs/frameworks.md#playwright)
68
+ * #### [Playwright](./docs/frameworks.md#playwright)
68
69
  * #### [CodeceptJS](./docs/frameworks.md#CodeceptJS)
69
70
  * #### [Cypress](./docs/frameworks.md#Cypress)
70
71
  * #### [Jest](./docs/frameworks.md#Jest)
@@ -99,7 +100,11 @@ GitHub report published as a comment to Pull Request:
99
100
  1. Create bucket on AWS, Google Cloud, or any other cloud storage provider supporting S3 protocol.
100
101
  2. [Pass S3 credentials](./docs/artifacts.md) to reporter to enable artifacts uploading.
101
102
 
102
- ### 4️⃣ Add to CI Pipeline
103
+ ### 4️⃣ Use Logger
104
+
105
+ Intercept your logger messages or log anything with our [Logger](./docs/logger.md) (_work in progress_).
106
+
107
+ ### 5️⃣ Add to CI Pipeline
103
108
 
104
109
  After you tested reporter locally add it to your CI pipeline.
105
110
 
@@ -119,6 +124,7 @@ Bring this reporter on CI and never lose test results again!
119
124
  * πŸ““ [JUnit](./docs/junit.md)
120
125
  * πŸ—„οΈ [Artifacts](./docs/artifacts.md)
121
126
  * πŸ”‚ [Workflows](./docs/workflows.md)
127
+ * πŸ”‚ [Logger](./docs/logger.md)
122
128
 
123
129
  ## Development
124
130
 
@@ -7,24 +7,29 @@ const { TESTOMAT_ARTIFACT_SUFFIX } = require('./constants');
7
7
  const { specificTestInfo } = require('./util');
8
8
 
9
9
  class ArtifactStorage {
10
-
10
+
11
11
  constructor(params) {
12
- this._tmpPrefix = "tsmt_reporter";
13
12
  this.isFile = false;
14
13
  this.isMemory = true;
15
14
  this.storage = params?.toFile;
16
15
 
17
16
  if (this.storage) {
17
+ // this is fine to use when you start saving artifacts;
18
+ // but when you need to retrieve them, you will not know if there is "isFile" param,
19
+ // because you need to create a new instance of ArtifactStorage inside client
20
+ // so the solution is make it opposite to isMemory (which will be retrieved from global)
18
21
  this.isFile = true;
19
22
  this.isMemory = false;
20
- this.tmpDirFullpath = this.createTestomatTmpDir(this._tmpPrefix);
23
+ this.tmpDirFullpath = this.createTestomatTmpDir(ArtifactStorage._tmpPrefix);
21
24
  debug('SAVE to tmp folder mode enabled!');
22
25
  }
23
26
  }
24
27
 
25
28
  static async storeToFile(tmpDirName, artifact, testSuffix = "test") {
29
+ // this assumes to use multiple files for each test. do we really want this?
26
30
  const suffix = TESTOMAT_ARTIFACT_SUFFIX + uuid.v4() + testSuffix;
27
31
  const dirpath = join(os.tmpdir(), tmpDirName);
32
+ // why json?
28
33
  const filepath = resolve(dirpath, `${suffix}.json`);
29
34
 
30
35
  return fs.promises.appendFile(filepath, JSON.stringify(artifact));
@@ -34,6 +39,7 @@ class ArtifactStorage {
34
39
  // TODO: mode for different pre-hooks. As variant - "all-tests" || "only_fail"
35
40
  // const mode = process.env.TESTOMAT_ARTIFACTS || "only_fail";
36
41
 
42
+ // you save the artifact without specifying its source - test id
37
43
  if (Array.isArray(global.testomatioArtifacts)) {
38
44
  debug("Saving artifacts to global storage");
39
45
 
@@ -71,6 +77,7 @@ class ArtifactStorage {
71
77
  return list;
72
78
  }
73
79
 
80
+ // returns all the content from all files; do we really need it?
74
81
  async tmpContents() {
75
82
  const contents = [];
76
83
 
@@ -103,8 +110,9 @@ class ArtifactStorage {
103
110
  }
104
111
  }
105
112
 
113
+ // no need to use multiple dirs; we can implement it later if required
106
114
  static tmpTestomatDirNames() {
107
- const subname = this._tmpPrefix || "tsmt_reporter";
115
+ const subname = ArtifactStorage._tmpPrefix || "tsmt_reporter";
108
116
 
109
117
  return fs.readdirSync(os.tmpdir(), { withFileTypes: true })
110
118
  .filter((item) => item.isDirectory())
@@ -113,6 +121,7 @@ class ArtifactStorage {
113
121
  }
114
122
 
115
123
  cleanup() {
124
+ // when you do a cleanup, I know nothing about isFile param
116
125
  if (this.isFile && this.tmpDirFullpath) {
117
126
  const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
118
127
 
@@ -128,4 +137,6 @@ class ArtifactStorage {
128
137
  }
129
138
  }
130
139
 
140
+ ArtifactStorage._tmpPrefix = "tsmt_reporter";
141
+
131
142
  module.exports = ArtifactStorage;
@@ -154,7 +154,7 @@ function CodeceptReporter(config) {
154
154
  files,
155
155
  steps: output.text(),
156
156
  }).then(pipes => {
157
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
157
+ testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
158
158
 
159
159
  debug("artifacts", artifacts);
160
160
 
@@ -19,7 +19,7 @@ class CucumberReporter extends Formatter {
19
19
  this.failures = [];
20
20
  this.cases = [];
21
21
 
22
- this.client = new TestomatClient();
22
+ this.client = new TestomatClient({ apiKey: process.env.TESTOMATIO || options.apiKey });
23
23
  this.status = STATUS.PASSED;
24
24
 
25
25
  }
@@ -1,6 +1,6 @@
1
1
  const TestomatClient = require('../client');
2
- const { STATUS } = require('../constants');
3
- const { parseTest, ansiRegExp } = require('../util');
2
+ const { STATUS, TESTOMAT_TMP_STORAGE } = require('../constants');
3
+ const { parseTest, ansiRegExp, fileSystem } = require('../util');
4
4
 
5
5
  class JestReporter {
6
6
  constructor(globalConfig, options) {
@@ -11,6 +11,17 @@ class JestReporter {
11
11
  this.client.createRun();
12
12
  }
13
13
 
14
+ static getIdOfCurrentlyRunningTest() {
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
18
+ }
19
+
20
+ onRunStart() {
21
+ // clear tmp dir
22
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
23
+ }
24
+
14
25
  onTestResult(test, testResult) {
15
26
  if (!this.client) return;
16
27
 
@@ -47,6 +58,9 @@ class JestReporter {
47
58
  const { numFailedTests } = results;
48
59
  const status = numFailedTests === 0 ? STATUS.PASSED : STATUS.FAILED;
49
60
  this.client.updateRunStatus(status);
61
+
62
+ // clear tmp dir
63
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
50
64
  }
51
65
  }
52
66
 
@@ -5,7 +5,7 @@ const chalk = require('chalk');
5
5
  const TestomatClient = require('../client');
6
6
  const { STATUS } = require('../constants');
7
7
  const { parseTest, specificTestInfo } = require('../util');
8
- const ArtifactStorage = require('../ArtifactStorage');
8
+ const ArtifactStorage = require('../_ArtifactStorageOld');
9
9
 
10
10
  const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } = Mocha.Runner.constants;
11
11
 
@@ -46,7 +46,7 @@ class TestomatioReporter {
46
46
  steps: steps.join('\n'),
47
47
  time: duration,
48
48
  }).then(pipes => {
49
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
49
+ testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
50
50
 
51
51
  this.uploads.push({
52
52
  testId, title, suite_title, files: result.attachments.filter((a) => a.body || a.path)
@@ -0,0 +1,25 @@
1
+ // const debug = require('debug')('@testomatio/reporter:logger');
2
+ const { DataStorage } = require('./dataStorage');
3
+
4
+ class ArtifactStorage {
5
+ constructor(params = {}) {
6
+ this.dataStorage = new DataStorage('artifact', params);
7
+
8
+ // singleton
9
+ if (!ArtifactStorage.instance) {
10
+ ArtifactStorage.instance = this;
11
+ }
12
+ }
13
+
14
+ save(data, context = null) {
15
+ this.dataStorage.putData(data, context);
16
+ }
17
+
18
+ get(context) {
19
+ this.dataStorage.getData(context);
20
+ }
21
+ }
22
+
23
+ ArtifactStorage.instance = null;
24
+
25
+ module.exports = new ArtifactStorage();
package/lib/client.js CHANGED
@@ -11,7 +11,7 @@ const pipesFactory = require('./pipe');
11
11
  /**
12
12
  * @typedef {import('../types').TestData} TestData
13
13
  * @typedef {import('../types').RunStatus} RunStatus
14
- * @typedef {import('../types').PipeResult} PipeResult
14
+ * @typedef {import('../types').PipeResult} PipeResult
15
15
  */
16
16
 
17
17
  class Client {
@@ -38,7 +38,7 @@ class Client {
38
38
  */
39
39
  createRun() {
40
40
  // all pipes disabled, skipping
41
- if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
41
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
42
42
 
43
43
  const runParams = {
44
44
  title: this.title,
@@ -47,6 +47,7 @@ class Client {
47
47
  };
48
48
 
49
49
  global.testomatioArtifacts = [];
50
+ // TODO: create global storage
50
51
 
51
52
  this.queue = this.queue
52
53
  .then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))))
@@ -58,7 +59,7 @@ class Client {
58
59
 
59
60
  /**
60
61
  * Updates test status and its data
61
- *
62
+ *
62
63
  * @param {string|undefined} status
63
64
  * @param {TestData} [testData]
64
65
  * @param {string[]} [storeArtifacts]
@@ -66,12 +67,13 @@ class Client {
66
67
  */
67
68
  async addTestRun(status, testData, storeArtifacts = []) {
68
69
  // all pipes disabled, skipping
69
- if (!this.pipes.filter(p => p.isEnabled).length) return;
70
+ if (!this.pipes?.filter(p => p.isEnabled).length) return [];
70
71
 
71
- if (!testData) testData = {
72
- title: 'Unknown test',
73
- suite_title: 'Unknown suite',
74
- }
72
+ if (!testData)
73
+ testData = {
74
+ title: 'Unknown test',
75
+ suite_title: 'Unknown suite',
76
+ };
75
77
 
76
78
  const {
77
79
  error = null,
@@ -100,13 +102,19 @@ class Client {
100
102
  stack = this.formatSteps(stack, steps);
101
103
  }
102
104
 
105
+ const logger = require('./logger');
106
+ const testLogs = logger.getLogs(test_id);
107
+ debug(`Test logs for ${test_id}:\n`, testLogs);
108
+ if (stack) stack += '\n\n';
109
+ stack += testLogs;
110
+
103
111
  if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
104
- debug("CLIENT storeArtifact", storeArtifacts);
112
+ debug('CLIENT storeArtifact', storeArtifacts);
105
113
  files.push(...storeArtifacts);
106
114
  }
107
115
 
108
116
  if (Array.isArray(global.testomatioArtifacts)) {
109
- debug("CLIENT global[testomatioArtifacts]", global.testomatioArtifacts);
117
+ debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
110
118
  files.push(...global.testomatioArtifacts);
111
119
  global.testomatioArtifacts = [];
112
120
  }
@@ -142,17 +150,19 @@ class Client {
142
150
  artifacts,
143
151
  };
144
152
 
145
- this.queue = this.queue
146
- .then(() => Promise.all(this.pipes.map(async p => {
147
- try {
148
- const result = await p.addTest(data);
149
- return { pipe: p.toString(), result };
150
- } catch (err) {
151
- console.log(APP_PREFIX, p.toString(), err);
152
- }
153
- }
154
- )));
155
-
153
+ this.queue = this.queue.then(() =>
154
+ Promise.all(
155
+ this.pipes.map(async p => {
156
+ try {
157
+ const result = await p.addTest(data);
158
+ return { pipe: p.toString(), result };
159
+ } catch (err) {
160
+ console.log(APP_PREFIX, p.toString(), err);
161
+ }
162
+ }),
163
+ ),
164
+ );
165
+
156
166
  return this.queue;
157
167
  }
158
168
 
@@ -165,15 +175,15 @@ class Client {
165
175
  */
166
176
  updateRunStatus(status, isParallel = false) {
167
177
  // all pipes disabled, skipping
168
- if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
178
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
169
179
 
170
180
  const runParams = { status, parallel: isParallel };
171
181
 
172
182
  this.queue = this.queue
173
183
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
174
184
  .then(() => {
175
- debug("TOTAL uploaded files", this.totalUploaded);
176
-
185
+ debug('TOTAL uploaded files', this.totalUploaded);
186
+
177
187
  if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
178
188
  console.log(
179
189
  APP_PREFIX,
package/lib/constants.js CHANGED
@@ -3,6 +3,10 @@ const chalk = require('chalk');
3
3
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
4
4
  const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
5
5
 
6
+ const TESTOMAT_TMP_STORAGE = {
7
+ mainDir: "testomatio_tmp",
8
+ }
9
+
6
10
  const CSV_HEADERS = [
7
11
  { id: 'suite_title', title: 'Suite_title' },
8
12
  { id: 'title', title: 'Title' },
@@ -21,6 +25,7 @@ const STATUS = {
21
25
  module.exports = {
22
26
  APP_PREFIX,
23
27
  TESTOMAT_ARTIFACT_SUFFIX,
28
+ TESTOMAT_TMP_STORAGE,
24
29
  CSV_HEADERS,
25
30
  STATUS,
26
31
  }
@@ -0,0 +1,180 @@
1
+ const debug = require('debug')('@testomatio/reporter:storage');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { join } = require('path');
6
+ const JestReporter = require('./adapter/jest');
7
+ const { TESTOMAT_TMP_STORAGE } = require('./constants');
8
+ const { fileSystem } = require('./util');
9
+
10
+ class DataStorage {
11
+ /**
12
+ * Creates data storage instance for specific data type.
13
+ * Stores data to global variable or to file depending on what is applicable for current test runner.
14
+ * dataType: 'log' | 'artifact' | ...
15
+ * @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
16
+ * @param {*} params
17
+ */
18
+ constructor(dataType, params = {}) {
19
+ if (!dataType) throw new Error('Data type is required when creating data storage');
20
+ this.dataType = dataType || 'data';
21
+ this.dataDirName = `${dataType}s`;
22
+ this.isFileStorage = params?.isFileStorage ?? !global.testomatioDataStore;
23
+
24
+ /*
25
+ FYI:
26
+ If this storage instance is used within test runner, it works fine.
27
+ But if, for example, we create storage instance inside the testomatio client (to get stored data),
28
+ the environment could be different (not already a test runner env).
29
+ Thus, checking environment is only reasonable when you put data to starage
30
+ and potentially useless when get data from storage
31
+ */
32
+ 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
+
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;
47
+ }
48
+
49
+ /**
50
+ * Try to define the running environment. Not 100% reliable and used as additional check
51
+ * @returns jest | mocha | ...
52
+ */
53
+ getRunningEnviroment() {
54
+ if (process.env.JEST_WORKER_ID) return 'jest';
55
+ try {
56
+ // @ts-expect-error mocha is defined only in mocha environment
57
+ if (typeof mocha !== 'undefined') return 'mocha';
58
+ } catch (e) {
59
+ // ignore
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Puts any data to storage (file or global variable)
66
+ * @param {*} data anything you want to store
67
+ * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
68
+ * @returns
69
+ */
70
+ putData(data, context = null) {
71
+ // this.isFileStorage = !global.testomatioDataStore;
72
+
73
+ let testId = null;
74
+ if (typeof context === 'string') {
75
+ testId = context;
76
+ } else {
77
+ // TODO: derive testId from context
78
+ // testId = context...
79
+ }
80
+
81
+ // try to get testId for Jest
82
+ if (this.runningEnvironment === 'jest') testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
83
+
84
+ // if testId is not provided, data is be saved to `{dataType}_other` file;
85
+ if (!testId) testId = 'other';
86
+
87
+ if (this.isFileStorage) {
88
+ this._putDataToFile(data, testId);
89
+ } else {
90
+ this._putDataToGlobalVar(data, testId);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Returns data, stored for specific testId (or data which was stored without test id specified)
96
+ *
97
+ * (Don't try to guess the execution environment (e.g. test runner) inside this method, it could be any)
98
+ * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
99
+ * @returns
100
+ */
101
+ getData(context) {
102
+ let testId = null;
103
+ if (typeof context === 'string') {
104
+ testId = context;
105
+ } else {
106
+ // TODO: derive testId from context
107
+ // testId = context...
108
+ }
109
+
110
+ if (!testId) {
111
+ debug(`Cannot get test id from passed context:\n}`, context);
112
+ }
113
+
114
+ if (this.isFileStorage) {
115
+ return this._getDataFromFile(testId);
116
+ }
117
+ if (global?.testomatioDataStore) {
118
+ return this._getDataFromGlobalVar(testId);
119
+ }
120
+ debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
121
+ return null;
122
+ }
123
+
124
+ _getDataFromGlobalVar(testId) {
125
+ try {
126
+ if (global?.testomatioDataStore[this.dataDirName]) {
127
+ const testData = global.testomatioDataStore[this.dataDirName][testId];
128
+ debug(`Data for test id ${testId}:\n${testData}`);
129
+ return testData;
130
+ }
131
+ debug(`No ${this.dataType} data for test id ${testId}`);
132
+ return null;
133
+ } catch (e) {
134
+ // there could be no data, ignore
135
+ }
136
+ }
137
+
138
+ _getDataFromFile(testId) {
139
+ try {
140
+ const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, `${this.dataType}_${testId}`);
141
+ if (fs.existsSync(filepath)) {
142
+ const testData = fs.readFileSync(filepath, 'utf-8');
143
+ debug(`Data for test id ${testId}:\n${testData}`);
144
+ return testData;
145
+ }
146
+ debug(`No ${this.dataType} data for test id ${testId}`);
147
+ return null;
148
+ } catch (e) {
149
+ // there could be no data, ignore
150
+ }
151
+ return null;
152
+ }
153
+
154
+ _putDataToGlobalVar(data, testId) {
155
+ debug('Saving data to global variable for test', testId, ':\n', data, '\n');
156
+ global.testomatioDataStore[this.dataDirName] = {};
157
+ global.testomatioDataStore[this.dataDirName][testId] = data;
158
+ }
159
+
160
+ _putDataToFile(data, testId) {
161
+ if (typeof data !== 'string') data = JSON.stringify(data);
162
+ const filename = `${this.dataType}_${testId}`;
163
+ const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, filename);
164
+ debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
165
+
166
+ // TODO: handle multiple invocations of JSON.stringify.
167
+ // UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
168
+ fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
169
+ }
170
+ }
171
+
172
+ module.exports.DataStorage = DataStorage;
173
+
174
+ // TODO: consider using fs promises instead of writeSync/appendFileSync to
175
+ // prevent blocking and improve performance (probably queue usage will be required)
176
+
177
+ // TODO: rewrite client - everything regarding storing artifacts
178
+ // TODO: try to define adapter inside client
179
+ // TODO: use .env
180
+ // TODO: ability to intercept multiple loggers
package/lib/logger.js ADDED
@@ -0,0 +1,278 @@
1
+ const chalk = require('chalk');
2
+ const debug = require('debug')('@testomatio/reporter:logger');
3
+ const { DataStorage } = require('./dataStorage');
4
+
5
+ const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
6
+ const LEVELS = {
7
+ ALL: { severity: 1, color: '' },
8
+ VERBOSE: { severity: 3, color: 'grey' },
9
+ TRACE: { severity: 5, color: 'grey' },
10
+ DEBUG: { severity: 7, color: 'cyan' },
11
+ INFO: { severity: 9, color: 'black' },
12
+ LOG: { severity: 11, color: 'black' },
13
+ WARN: { severity: 13, color: 'yellow' },
14
+ ERROR: { severity: 15, color: 'red' },
15
+ };
16
+
17
+ /**
18
+ * Logger allows to:
19
+ * 1. Intercept logs from user logger (console.log, etc) and store them.
20
+ * 2. Output logs to console (actually, logger functionality).
21
+ * 3. Varied syntax.
22
+ */
23
+ class Logger {
24
+ // _originalUserLogger used to output logs to console by the user logger
25
+ // _loggerToIntercept intercepted and reassigned immediately when added
26
+
27
+ constructor(params = {}) {
28
+ // set default logger to be used in log, warn, error, etc methods
29
+ this._originalUserLogger = { ...console };
30
+
31
+ this.dataStorage = new DataStorage('log', params);
32
+ this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
33
+
34
+ // commented because prefer to use "intercept" method
35
+ // if (params?.logger) this._loggerToIntercept = params.logger;
36
+
37
+ this.intercept(this._loggerToIntercept);
38
+
39
+ // singleton
40
+ if (!Logger.instance) {
41
+ Logger.instance = this;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Tagget template literal. Allows to use different syntaxes:
47
+ * 1. Tagget template: $`text ${someVar}`
48
+ * 2. Standard: $(`text ${someVar}`)
49
+ * 3. Standard with multiple arguments: $('text', someVar)
50
+ */
51
+ _log(strings, ...args) {
52
+ let logs;
53
+ // this block means tagged template is used (syntax like $`text ${someVar}`)
54
+ if (Array.isArray(strings) && strings.length === args.length + 1) {
55
+ logs = strings.reduce(
56
+ (result, current, index) =>
57
+ result +
58
+ current +
59
+ // strings are splitted by args when use tagged template, thus we add arg after each string
60
+ // it looks like: `string1 arg1 string2 arg2 string3`
61
+ (args[index] !== undefined // eslint-disable-line no-nested-ternary
62
+ ? typeof args[index] === 'string'
63
+ ? args[index] // add arg as it is
64
+ : this._strinfifyLogs(args[index]) // stringify arg
65
+ : ' '), // add space if no arg after string
66
+ // initial accumulator value
67
+ '',
68
+ );
69
+ } else {
70
+ // this block means arguments syntax is used (syntax like $('text', someVar))
71
+ // in this case strings represents just a first argument
72
+ logs = this._strinfifyLogs(strings, ...args);
73
+ }
74
+ this.dataStorage.putData(logs);
75
+ }
76
+
77
+ /**
78
+ * Allows you to define a step inside a test. Step name is attached to the report and
79
+ * helps to understand the test flow.
80
+ * @param {*} strings
81
+ * @param {...any} values
82
+ */
83
+ step(strings, ...values) {
84
+ let logs = '';
85
+ for (let i = 0; i < strings.length; i++) {
86
+ logs += strings[i];
87
+ if (i < values.length) {
88
+ logs += values[i];
89
+ }
90
+ }
91
+ logs = chalk.blue(`> ${logs}`);
92
+ this.dataStorage.putData(logs);
93
+ }
94
+
95
+ /**
96
+ *
97
+ * @param {*} context testId or test context from test runner
98
+ * @returns
99
+ */
100
+ getLogs(context) {
101
+ return this.dataStorage.getData(context);
102
+ }
103
+
104
+ _strinfifyLogs(...args) {
105
+ const logs = [];
106
+ // stringify everything except strings
107
+ for (const arg of args) {
108
+ if (typeof arg === 'string') {
109
+ logs.push(arg);
110
+ } else {
111
+ try {
112
+ // eslint-disable-next-line no-unused-expressions
113
+ this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
114
+ } catch (e) {
115
+ debug('Error while stringify object', e);
116
+ logs.push(arg);
117
+ }
118
+ }
119
+ }
120
+ return logs.join(' ');
121
+ }
122
+
123
+ assert(...args) {
124
+ const level = 'ERROR';
125
+ const severity = LEVELS[level].severity;
126
+ if (severity < LEVELS[this.logLevel]?.severity) return;
127
+
128
+ const logs = this._strinfifyLogs(...args);
129
+ this.dataStorage.putData(logs);
130
+ try {
131
+ this._originalUserLogger.log(`Assertion result: `, ...args);
132
+ } catch (e) {
133
+ // method could be unexisting, ignore error
134
+ }
135
+ }
136
+
137
+ debug(...args) {
138
+ const level = 'DEBUG';
139
+ const severity = LEVELS[level].severity;
140
+ if (severity < LEVELS[this.logLevel]?.severity) return;
141
+
142
+ if (this.logLevel === 'error' || this.logLevel === 'warn') return;
143
+ const logs = this._strinfifyLogs(...args);
144
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
145
+ this.dataStorage.putData(colorizedLogs);
146
+ try {
147
+ this._originalUserLogger.debug(...args);
148
+ } catch (e) {
149
+ // method could be unexisting, ignore error
150
+ }
151
+ }
152
+
153
+ error(...args) {
154
+ const level = 'ERROR';
155
+ const severity = LEVELS[level].severity;
156
+ if (severity < LEVELS[this.logLevel]?.severity) return;
157
+
158
+ const logs = this._strinfifyLogs(...args);
159
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
160
+ this.dataStorage.putData(colorizedLogs);
161
+ try {
162
+ this._originalUserLogger.error(...args);
163
+ } catch (e) {
164
+ // method could be unexisting, ignore error
165
+ }
166
+ }
167
+
168
+ info(...args) {
169
+ const level = 'INFO';
170
+ const severity = LEVELS[level].severity;
171
+ if (severity < LEVELS[this.logLevel]?.severity) return;
172
+
173
+ const logs = this._strinfifyLogs(...args);
174
+ this.dataStorage.putData(logs);
175
+ try {
176
+ this._originalUserLogger.info(...args);
177
+ } catch (e) {
178
+ // method could be unexisting, ignore error
179
+ }
180
+ }
181
+
182
+ log(...args) {
183
+ const level = 'INFO';
184
+ const severity = LEVELS[level].severity;
185
+ if (severity < LEVELS[this.logLevel]?.severity) return;
186
+
187
+ const logs = this._strinfifyLogs(...args);
188
+ this.dataStorage.putData(logs);
189
+ try {
190
+ this._originalUserLogger.log(...args);
191
+ } catch (e) {
192
+ // method could be unexisting, ignore error
193
+ }
194
+ }
195
+
196
+ trace(...args) {
197
+ const level = 'TRACE';
198
+ const severity = LEVELS[level].severity;
199
+ if (severity < LEVELS[this.logLevel]?.severity) return;
200
+
201
+ const logs = this._strinfifyLogs(...args);
202
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
203
+ this.dataStorage.putData(colorizedLogs);
204
+ try {
205
+ this._originalUserLogger.trace(...args);
206
+ } catch (e) {
207
+ // method could be unexisting, ignore error
208
+ }
209
+ }
210
+
211
+ warn(...args) {
212
+ const level = 'WARN';
213
+ const severity = LEVELS[level].severity;
214
+ if (severity < LEVELS[this.logLevel]?.severity) return;
215
+
216
+ const logs = this._strinfifyLogs(...args);
217
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
218
+ this.dataStorage.putData(colorizedLogs);
219
+ try {
220
+ this._originalUserLogger.warn(...args);
221
+ } catch (e) {
222
+ // method could be unexisting, ignore error
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Intercepts user logger messages.
228
+ * When call this method, Logger start to control the user logger,
229
+ * but almost nothing is changed for user ragarding the console output (like log level set by user)
230
+ * (until multiple loggers are intercepted,
231
+ * in this case only the last intercepted logger will be used as user console output).
232
+ * @param {*} userLogger
233
+ */
234
+ intercept(userLogger) {
235
+ if (!userLogger) return;
236
+ debug(`Intercepting user logger`);
237
+
238
+ // save original user logger to use it for logging (last intercepted will be used for console output)
239
+ this._originalUserLogger = { ...userLogger };
240
+ this._loggerToIntercept = userLogger;
241
+
242
+ /*
243
+ override user logger (any, e.g. console) methods to intercept log messages
244
+ this._loggerToIntercept = this; could be used, but decided to override only output methods
245
+ */
246
+ for (const method of LOG_METHODS) {
247
+ /*
248
+ decided to comment next code line because its better to create method even if it does not exist in user logger;
249
+ on method invocation, we will store the data anyway and catch block will prevent potential errors
250
+ */
251
+ // if (!this._loggerToIntercept[method]) continue;
252
+ this._loggerToIntercept[method] = (...args) => this[method](...args);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Allows to configure logger. Make sure you do it before the logger usage in your code.
258
+ *
259
+ * @param {Object} [config={}] - The configuration object.
260
+ * @param {string} [config.logLevel] - The desired log level. Valid values are 'DEBUG', 'INFO', 'WARN', and 'ERROR'.
261
+ * @param {boolean} [config.prettyObjects] - Specifies whether to enable pretty printing of objects.
262
+ * @returns {void}
263
+ */
264
+ configure(config = {}) {
265
+ if (!config) return;
266
+ if (config.prettyObjects) this.prettyObjects = config.prettyObjects;
267
+ if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
268
+ }
269
+ }
270
+
271
+ Logger.instance = null;
272
+
273
+ // module.exports.Logger = Logger;
274
+ module.exports = new Logger();
275
+
276
+ // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
277
+ // upd: did not face such loggers, but still could be useful
278
+
package/lib/reporter.js CHANGED
@@ -1,8 +1,15 @@
1
+ const logger = require('./logger');
1
2
  const TestomatClient = require('./client');
2
3
  const TRConstants = require('./constants');
3
- const TRArtifacts = require('./ArtifactStorage');
4
+ const TRArtifacts = require('./_ArtifactStorageOld');
5
+
6
+ const log = logger._log.bind(logger);
7
+ const step = logger.step.bind(logger);
4
8
 
5
9
  module.exports = {
10
+ logger,
11
+ log,
12
+ step,
6
13
  TestomatClient,
7
14
  TRConstants,
8
15
  TRArtifacts,
package/lib/util.js CHANGED
@@ -3,6 +3,7 @@ const { sep, basename } = require('path');
3
3
  const chalk = require('chalk');
4
4
  const fs = require('fs');
5
5
  const isValid = require('is-valid-path');
6
+ const debug = require('debug')('@testomatio/reporter:util');
6
7
 
7
8
  /**
8
9
  * @param {String} testTitle - Test title
@@ -10,7 +11,10 @@ const isValid = require('is-valid-path');
10
11
  * @returns {String|null} testId
11
12
  */
12
13
  const parseTest = testTitle => {
14
+ if (!testTitle) return null;
15
+
13
16
  const captures = testTitle.match(/@T([\w\d]+)/);
17
+
14
18
  if (captures) {
15
19
  return captures[1];
16
20
  }
@@ -32,7 +36,6 @@ const parseSuite = suiteTitle => {
32
36
  return null;
33
37
  };
34
38
 
35
-
36
39
  const ansiRegExp = () => {
37
40
  const pattern = [
38
41
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -54,11 +57,14 @@ const isValidUrl = s => {
54
57
 
55
58
  const fetchFilesFromStackTrace = (stack = '') => {
56
59
  const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
57
- return Array.from(files).map(f => f[1]).filter(f => fs.existsSync(f));
58
- }
60
+ return Array.from(files)
61
+ .map(f => f[1])
62
+ .filter(f => fs.existsSync(f));
63
+ };
59
64
 
60
65
  const fetchSourceCodeFromStackTrace = (stack = '') => {
61
- const stackLines = stack.split('\n')
66
+ const stackLines = stack
67
+ .split('\n')
62
68
  .filter(l => l.includes(':'))
63
69
  // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
64
70
  // .map(l => l.split(':')[0])
@@ -67,17 +73,17 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
67
73
  .filter(l => isValid(l?.split(':')[0]))
68
74
 
69
75
  // // filter out 3rd party libs
70
- .filter(l => !l?.includes(`vendor${ sep}`))
71
- .filter(l => !l?.includes(`node_modules${ sep}`))
76
+ .filter(l => !l?.includes(`vendor${sep}`))
77
+ .filter(l => !l?.includes(`node_modules${sep}`))
72
78
  .filter(l => fs.existsSync(l.split(':')[0]))
73
- .filter(l => fs.lstatSync(l.split(':')[0]).isFile())
79
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile());
74
80
 
75
81
  if (!stackLines.length) return '';
76
82
 
77
83
  const [file, line] = stackLines[0].split(':');
78
84
 
79
85
  const prepend = 3;
80
- const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
86
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
81
87
 
82
88
  if (!source) return '';
83
89
 
@@ -85,22 +91,22 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
85
91
  .map((l, i) => {
86
92
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
87
93
  return `${line - prepend + i} | ${l}`
88
- }).join('\n')
89
- }
94
+ }).join('\n');
95
+ };
90
96
 
91
97
  const fetchSourceCode = (contents, opts = {}) => {
92
98
  if (!opts.title && !opts.line) return '';
93
99
 
94
- // code fragment is 20 lines
100
+ // code fragment is 20 lines
95
101
  const limit = opts.limit || 50;
96
102
  let lineIndex;
97
103
  if (opts.line) lineIndex = opts.line - 1;
98
- const lines = contents.split('\n')
104
+ const lines = contents.split('\n');
99
105
 
100
106
  // remove special chars from title
101
107
  if (!lineIndex && opts.title) {
102
- const title = opts.title.replace(/[([@].*/g, '')
103
- lineIndex = lines.findIndex(l => l.includes(title))
108
+ const title = opts.title.replace(/[([@].*/g, '');
109
+ lineIndex = lines.findIndex(l => l.includes(title));
104
110
  }
105
111
 
106
112
  if (opts.prepend) {
@@ -133,52 +139,69 @@ const fetchSourceCode = (contents, opts = {}) => {
133
139
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
134
140
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
135
141
  }
136
- result.push(lines[i])
142
+ result.push(lines[i]);
137
143
  }
138
144
  return result.join('\n');
139
145
  }
140
- }
146
+ };
141
147
 
142
- const isSameTest = (test, t) => (typeof t === 'object')
143
- && (typeof test === 'object')
144
- && t.title === test.title
145
- && t.suite_title === test.suite_title
146
- && Object.values(t.example || {}) === Object.values(test.example || {})
147
- && t.test_id === test.test_id;
148
+ const isSameTest = (test, t) =>
149
+ typeof t === 'object' &&
150
+ typeof test === 'object' &&
151
+ t.title === test.title &&
152
+ t.suite_title === test.suite_title &&
153
+ Object.values(t.example || {}) === Object.values(test.example || {}) &&
154
+ t.test_id === test.test_id;
148
155
 
149
156
  const getCurrentDateTime = () => {
150
157
  const today = new Date();
151
-
152
- return `${today.getFullYear() }_${ today.getMonth() + 1 }_${ today.getDate() }_${
153
- today.getHours() }_${ today.getMinutes() }_${ today.getSeconds()}`;
154
- }
158
+
159
+ return `${today.getFullYear()}_${
160
+ today.getMonth() + 1
161
+ }_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
162
+ };
155
163
 
156
164
  /**
157
165
  * @param {Object} test - Test adapter object
158
166
  *
159
- * @returns {String|null} testInfo as one string
167
+ * @returns {String|null} testInfo as one string
160
168
  */
161
169
  const specificTestInfo = test => {
162
170
  // TODO: afterEach has another context.... need to add specific handler, maybe...
163
171
  if (test?.title && test?.file) {
164
-
165
- return `${basename(test.file).split(".").join("#")
166
- }#${
167
- test.title.split(" ").join("#")}`;
172
+ return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
168
173
  }
169
174
 
170
175
  return null;
171
176
  };
172
177
 
178
+ const fileSystem = {
179
+ createDir(dirPath) {
180
+ if (!fs.existsSync(dirPath)) {
181
+ fs.mkdirSync(dirPath, { recursive: true });
182
+ debug('Created dir: ', dirPath);
183
+ }
184
+ },
185
+ clearDir(dirPath) {
186
+ if (fs.existsSync(dirPath)) {
187
+ fs.rmSync(dirPath, { recursive: true });
188
+ debug(`Dir ${dirPath} was deleted`);
189
+ } else {
190
+ debug(`Trying to delete ${dirPath} but it doesn't exist`);
191
+ }
192
+ }
193
+ };
194
+
173
195
  module.exports = {
174
196
  isSameTest,
175
197
  fetchSourceCode,
176
198
  fetchSourceCodeFromStackTrace,
177
199
  fetchFilesFromStackTrace,
200
+ fileSystem,
178
201
  getCurrentDateTime,
179
202
  specificTestInfo,
180
203
  isValidUrl,
181
204
  ansiRegExp,
182
205
  parseTest,
183
- parseSuite
184
- }
206
+ parseSuite,
207
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
package/Changelog.md DELETED
@@ -1,264 +0,0 @@
1
- # 1.0.0
2
-
3
- <!-- pending release updates -->
4
-
5
- * Added [`TESTOMATIO_SHARED_RUN` option](https://github.com/testomatio/reporter/blob/master/docs/pipes.md#reporting-parallel-execution-to-to-same-run) to use a shared run for parallel executions
6
- * Reworked [documentation](https://github.com/testomatio/reporter/tree/master#readme).
7
- * Added an option to obtain [S3 configuration](https://github.com/testomatio/reporter/blob/master/docs/artifacts.md#configuration) from Testomat.io
8
- * Introduced [pipes](https://github.com/testomatio/reporter/blob/master/docs/pipes.md):
9
- * GitHub
10
- * GitLab
11
- * CSV Pipe
12
-
13
-
14
- # 0.7.6
15
-
16
- * Updated to use AWS S3 3.0 SDK for uploading
17
-
18
- # 0.7.5
19
-
20
- * Fixed reporting skipped tests in mocha
21
-
22
- # 0.7.4
23
-
24
- * Fixed parsing source code in JUnit files
25
-
26
- # 0.7.3
27
-
28
- * CodeceptJS: Upload all traces and videos from artifacts
29
- * Fixed reporting skipped test in XML
30
- * added `--timelimit` option to `report-xml` command line
31
-
32
- # 0.7.2
33
-
34
- * Fixed uploading non-existing file
35
-
36
- # 0.7.1
37
-
38
- * Support for NUnit XML v3 format
39
-
40
- # 0.7.0
41
-
42
- * Support for `@cucumber/cucumber` (>= 7.0) added
43
- * Initial support for C# and NUnit
44
-
45
- # 0.6.10
46
-
47
- * Fixed uploading multilpe artifacts in Playwright
48
-
49
- # 0.6.9
50
-
51
- * Fixed pending tests reports for Cypress
52
-
53
- # 0.6.8
54
- # 0.6.7
55
-
56
- * Pytest: fixed creating suites from reports
57
-
58
- # 0.6.6
59
-
60
- * JUnit reporter: prefer suite title over testcase classname in a report
61
-
62
- # 0.6.5
63
-
64
- * Fixed test statuses for runs in JUnit reporter
65
-
66
- # 0.6.4
67
-
68
- * Added `TESTOMATIO_PROCEED=1` param to not close current run
69
- * Fixed priority of commands from `npx @testomatio/reporter`
70
-
71
- # 0.6.3
72
-
73
- * Fixed `npx start-test-run` to launch commands
74
-
75
- # 0.6.2
76
-
77
- * Added `--env-file` option to load env variables from env file
78
-
79
- # 0.6.1
80
-
81
- * Fixed creating RunGroup with JUnit reporter
82
-
83
- # 0.6.0
84
-
85
- * JUnit reporter support
86
-
87
- # 0.5.10
88
-
89
- * Fixed reporting Scenario Outline in Cypress-Cucumber
90
- * Fixed error reports for Cypress when running in Chrome
91
-
92
- # 0.5.9
93
-
94
- * Added environment on Cypress report
95
-
96
- # 0.5.8
97
-
98
- * Fixed Cypress.io reporting
99
-
100
- # 0.5.7
101
-
102
- * Fixed webdriverio artifacts
103
-
104
- # 0.5.6
105
-
106
- * Unmark failed CodeceptJS tests as skipped
107
-
108
- # 0.5.5
109
-
110
- * Fixed `BeforeSuite` failures in CodeceptJS
111
-
112
- # 0.5.4
113
-
114
- Added `TESTOMATIO_CREATE=1` option to create unmatched tests on report
115
-
116
- ```
117
- TESTOMATIO_CREATE=1 TESTOMATIO=apiKey npx codeceptjs run
118
- ```
119
-
120
- # 0.5.3
121
-
122
- * Fixed parsing suites
123
-
124
- # 0.5.2
125
-
126
- * Fixed multiple upload of artifacts in Cypress.io
127
-
128
- # 0.5.1
129
-
130
- * Fixed Cypress.io to report tests inside nested suites
131
-
132
- # 0.5.0
133
-
134
- * Added Cypress.io plugin
135
- * Added artifacts upload to webdriverio
136
-
137
- # 0.4.6
138
-
139
- - Fixed CodeceptJS reporter to report tests failed in hooks
140
-
141
- # 0.4.5
142
-
143
- - Fixed "Total XX artifacts publicly uploaded to S3 bucket" when no S3 bucket is configured
144
- - Improved S3 connection error messages
145
-
146
- # 0.4.4
147
-
148
- - Fixed returning 0 exit code when a process fails when running tests in parallel via `start-test-run`. Previously was using the last exit code returned by a process. Currently prefers the highest exit code that was returned by a process.
149
-
150
- # 0.4.3
151
-
152
- - Added `TESTOMATIO_DISABLE_ARTIFACTS` env variable to disable publishing artifacts.
153
-
154
- # 0.4.2
155
-
156
- - print version of reporter
157
- - print number of uploaded artifacts
158
- - print access mode for uploaded artifacts
159
-
160
- # 0.4.1
161
-
162
- Added `global.testomatioArtifacts = []` array which can be used to add arbitrary artifacts to a report.
163
-
164
- ```js
165
- // inside a running test:
166
- global.testomatioArtifacts.push('file/to/upload.png');
167
- ```
168
-
169
- # 0.4.0
170
-
171
- - Playwright: Introduced playwright/test support with screenshots and video artifacts
172
-
173
- > Known issues: reporting using projects configured in Playwright does not work yet
174
-
175
- - CodeceptJS: added video uploads
176
-
177
- # 0.3.16
178
-
179
- - CodeceptJS: fixed reporting tests with empty steps (on retry)
180
-
181
- # 0.3.15
182
-
183
- - Finish Run via API:
184
-
185
- ```
186
- TESTOMATIO={apiKey} TESTOMATIO_RUN={runId} npx @testomatio/reporter@latest --finish
187
- ```
188
-
189
- # 0.3.14
190
-
191
- - Create an empty Run via API:
192
-
193
- ```
194
- TESTOMATIO={apiKey} npx @testomatio/reporter@latest --launch
195
- ```
196
-
197
- # 0.3.13
198
-
199
- - Checking for a valid report URL
200
- - Sending unlimited data on test report
201
-
202
- # 0.3.12
203
-
204
- - Fixed submitting arbitrary data on a test run
205
- - Jest: fixed sending errors with stack traces
206
- - Cypress: fixed sending reports
207
-
208
- # 0.3.11
209
-
210
- - Fixed circular JSON reference when submitting data to Testomatio
211
-
212
- # 0.3.10
213
-
214
- - Minor fixes
215
-
216
- # 0.3.9
217
-
218
- - Making all reporters to run without API key
219
-
220
- # 0.3.8
221
-
222
- - Fixed `npx start-test-run` to work with empty API keys
223
-
224
- # 0.3.7
225
-
226
- - Fixed release
227
-
228
- # 0.3.6
229
-
230
- - Update title and rungroup on start for scheduled runs.
231
-
232
- # 0.3.5
233
-
234
- - Added `TESTOMATIO_RUN` environment variable to pass id of a specific run to report
235
-
236
- # 0.3.4
237
-
238
- - Minor fixes
239
-
240
- # 0.3.3
241
-
242
- - [CodeceptJS] Fixed stack trace reporting
243
- - [CodeceptJS] Fixed displaying of nested steps
244
- - [CodeceptJS][mocha] Added assertion diff to report
245
-
246
- # 0.3.2
247
-
248
- - Fixed error message for S3 uploading
249
-
250
- # 0.3.1
251
-
252
- - [CodeceptJS] Better formatter for nested structures and BDD tests
253
-
254
- # 0.3.0
255
-
256
- - Added `TESTOMATIO_TITLE` env variable to set a name for Run
257
- - Added `TESTOMATIO_RUNGROUP_TITLE` env variable to attach Run to RunGroup
258
- - Added `TESTOMATIO_ENV` env variable to attach additional env values to report
259
- - [CodeceptJS] **CodeceptJS v3 support**
260
- - [CodeceptJS] Dropped support for CodeceptJS 2
261
- - [CodeceptJS] Added support for before hooks
262
- - [CodeceptJS] Log of steps
263
- - [CodeceptJS] Upload screenshots of failed tests to S3
264
- - [CodeceptJS] Updated to use with parallel execution