@testomatio/reporter 1.1.0-beta.mocha-create.8 → 1.1.1-beta-codecept-logger

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.
@@ -4,225 +4,108 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const { join } = require('path');
6
6
  const { TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
7
- const { fileSystem, jestHelpers, parseTest } = require('../utils/utils');
8
-
9
- const getTestIdFromTestTitle = parseTest;
7
+ const { fileSystem } = require('../utils/utils');
10
8
 
11
9
  class DataStorage {
12
10
  /**
13
11
  * Creates data storage instance for specific data type.
14
12
  * Stores data to global variable or to file depending on what is applicable for current test runner
15
- * (running environment).
16
13
  * Recommend to use composition while using this class (instead of inheritance).
17
14
  * ! Also the class which will use data storage should be singleton (to avoid data loss).
18
- * @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
15
+ * But the data storage itself is not singleton. It will have an instance per data type.
16
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType – type of data to store
19
17
  */
20
18
  constructor(dataType) {
21
19
  this.dataType = dataType || 'data';
20
+
21
+ // some frameworks use global variable to store data, some use file storage
22
22
  this.isFileStorage = true;
23
- this.#refreshStorageType();
24
23
 
25
24
  this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, this.dataType);
26
-
27
- // set test title for mocha (could not be moved to adapter)
28
- // it does not override existing hook, just add new one
29
- try {
30
- // @ts-ignore
31
- if (!beforeEach) return;
32
- // @ts-ignore-next-line
33
- beforeEach(function () {
34
- if (this.currentTest?.__mocha_id__) {
35
- global.testTitle = this.currentTest.fullTitle();
36
- }
37
- });
38
- } catch (e) {
39
- // ignore
40
- }
41
-
42
- // set test title for playwright
43
- try {
44
- // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
45
- const { test } = require('@playwright/test');
46
- // eslint-disable-next-line no-empty-pattern
47
- test.beforeEach(async ({}, testInfo) => {
48
- global.testTitle = testInfo.title;
49
- global.testomatioRunningEnvironment = 'playwright';
50
- });
51
- } catch (e) {
52
- // ignore
53
- }
54
25
  }
55
26
 
56
- /**
57
- * Try to define the running environment. Not 100% reliable and used as additional check
58
- * @returns jest | mocha | ...
59
- */
60
- getRunningEnviroment() {
61
- // jest
62
- if (process.env.JEST_WORKER_ID) return 'jest';
63
-
64
- if (global.codeceptjs) return 'codeceptjs';
65
-
66
- // 'cucumber:current', 'cucumber:legacy'
67
- if (global.testomatioRunningEnvironment) return global.testomatioRunningEnvironment;
68
-
69
- if (process.env.PLAYWRIGHT_TEST_BASE_URL || process.env.TEST_WORKER_INDEX) return 'playwright';
70
-
71
- // mocha - can't detect
72
- return null;
27
+ #stringToFilename(str) {
28
+ // TODO: use md5 hash later
29
+ const validFilenameRegex = /[^a-zA-Z0-9_.-]/g;
30
+ // replace all characters not in the regex above with underscore, then duplicate underscores removed
31
+ let filename = str.replace(validFilenameRegex, '_').replace(/_{2,}/g, '_').substring(0, 255); // max filename length
32
+ // remove leading and trailing underscores
33
+ filename = filename.replace(/^_+|_+$/g, '');
34
+ return filename;
73
35
  }
74
36
 
75
37
  /**
76
38
  * Puts any data to storage (file or global variable).
77
39
  * If file: stores data as text, if global variable – stores as array of data.
78
40
  * @param {*} data anything you want to store (string, object, array, etc)
79
- * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
41
+ * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
42
+ * suite name + test name is used by default
80
43
  * @returns
81
44
  */
82
45
  putData(data, context = null) {
83
46
  fileSystem.createDir(this.dataDirPath);
84
-
85
- this.#refreshStorageType();
86
- context = context ?? global.testTitle ?? null;
87
- let testId = this.#tryToRetrieveTestId(context) || context || null;
88
-
89
- if (this.runningEnvironment === 'codeceptjs') {
90
- this.isFileStorage = false;
91
- testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
92
- }
93
-
94
- if (this.runningEnvironment === 'jest') {
95
- testId = testId ?? jestHelpers.getIdOfCurrentlyRunningTest();
96
- }
97
-
98
- // logs in playwright are gathered by pw framework itself
99
- if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
100
-
101
- // get id from global store
102
- if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
103
- testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
104
-
105
- if (!testId && global?.currentlyRunningTestTitle)
106
- testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
107
-
108
- if (!testId) {
109
- debug(`No test id provided for "${this.dataType}" data:`, data);
47
+ if (!context) {
48
+ debug(`No context provided for "${this.dataType}" data:`, data);
110
49
  return;
111
50
  }
51
+ context = this.#stringToFilename(context);
112
52
 
113
53
  if (this.isFileStorage) {
114
- this.#putDataToFile(data, testId);
54
+ this.#putDataToFile(data, context);
115
55
  } else {
116
- this.#putDataToGlobalVar(data, testId);
56
+ this.#putDataToGlobalVar(data, context);
117
57
  }
118
58
  }
119
59
 
120
60
  /**
121
- * Returns data, stored for specific testId (or data which was stored without test id specified).
61
+ * Returns data, stored for specific test/context (or data which was stored without test id specified).
122
62
  * This method will get data from global variable and/or from from file (previosly saved with put method).
123
63
  *
124
- * @param {*} context could be testId or any context (test title, suite, etc) which is used to define testId
125
- * @returns array of data (any type), null (if no data found for test) or string (if data type is log)
64
+ * @param {string} context
65
+ * @returns {any []} of data (any type), null (if no data found for test) or string (if data type is log)
126
66
  */
127
67
  getData(context) {
128
- this.#refreshStorageType();
129
-
130
- let testId = null;
131
- if (typeof context === 'string') {
132
- testId = this.#tryToRetrieveTestId(context) || context;
133
- }
134
-
135
- if (!testId) {
136
- debug(`Cannot get test id from passed context:`, context);
68
+ if (!context) {
69
+ debug(`No context passed`, context);
137
70
  return null;
138
71
  }
139
72
 
73
+ context = this.#stringToFilename(context);
74
+
140
75
  let testDataFromFile = [];
141
76
  let testDataFromGlobalVar = [];
142
77
 
143
78
  if (global?.testomatioDataStore) {
144
- testDataFromGlobalVar = this.#getDataFromGlobalVar(testId);
145
- // these frameworks use global variable storage
146
- // if (testDataFromGlobalVar && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) {
79
+ testDataFromGlobalVar = this.#getDataFromGlobalVar(context);
147
80
  if (testDataFromGlobalVar) {
148
- // return as string if its log data
149
- if (this.dataType === 'log' && testDataFromGlobalVar.length) return testDataFromGlobalVar.join('\n');
150
- // return as array for other data types
151
81
  if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
152
82
  }
153
83
  // don't return nothing if no data in global variable
154
84
  }
155
85
 
156
- /* condition is removed for mocha
157
- mocha has created a global storage, but just in some cases, generally it is not available
158
- */
159
- // if (this.isFileStorage || !testData) {
160
- // testData = this.#getDataFromFile(testId);
161
- // if (testData) return testData;
162
- // }
163
-
164
- testDataFromFile = this.#getDataFromFile(testId);
86
+ testDataFromFile = this.#getDataFromFile(context);
165
87
 
166
88
  if (testDataFromFile.length) {
167
- if (this.dataType === 'log') return testDataFromFile.join('\n');
168
89
  return testDataFromFile;
169
90
  }
170
- debug(`No "${this.dataType}" data for test id ${testId} in both file and global variable`);
91
+ debug(`No "${this.dataType}" data for test id ${context} in both file and global variable`);
171
92
 
172
93
  // in case no data found for test
173
94
  return null;
174
95
  }
175
96
 
176
97
  /**
177
- * This method is named as "try" because it does not guarantee that test id could be retrieved.
178
- * Context could be anything (test, suite, string, etc) which is used to define testId.
179
- * Or it could represent any other entity (which does not contain test id).
180
- * @param {*} context
181
- */
182
- #tryToRetrieveTestId(context) {
183
- if (!context) return null;
184
- this.#refreshStorageType();
185
-
186
- if (this.runningEnvironment === 'playwright' || context?.title) {
187
- // context is testInfo
188
- const testId = getTestIdFromTestTitle(context.title);
189
- if (testId) return testId;
190
- }
191
-
192
- if (typeof context === 'string') {
193
- const testId = getTestIdFromTestTitle(context);
194
- if (testId) return testId;
195
- }
196
-
197
- return null;
198
- }
199
-
200
- /**
201
- * Refreshes storage type (file or global variable) depending on current environment
202
- * This method should be run on each attempt to put or get data from/to storage
203
- * Because storage instance is created before the test runner is started. And storage type could be changed
204
- */
205
- #refreshStorageType() {
206
- this.runningEnvironment = this.getRunningEnviroment();
207
-
208
- // some test frameworks do not persist global variables, thus file storage is used for them (by default)
209
- if (this.runningEnvironment === 'codeceptjs') this.isFileStorage = false;
210
- // playwrigth stores logs by itself; but file storage is used for other data types for playwright
211
- if (this.runningEnvironment === 'playwright' && this.dataType === 'log') this.isFileStorage = false;
212
- }
213
-
214
- /**
215
- * @param {*} testId
98
+ * @param {string} context
216
99
  * @returns aray of data (any type)
217
100
  */
218
- #getDataFromGlobalVar(testId) {
101
+ #getDataFromGlobalVar(context) {
219
102
  try {
220
103
  if (global?.testomatioDataStore[this.dataType]) {
221
- const testData = global.testomatioDataStore[this.dataType][testId];
222
- if (testData) debug(`"${this.dataType}" data for test id ${testId}:`, testData.join(', '));
104
+ const testData = global.testomatioDataStore[this.dataType][context];
105
+ if (testData) debug(`"${this.dataType}" data for test id ${context}:`, testData.join(', '));
223
106
  return testData || [];
224
107
  }
225
- // debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
108
+ // debug(`No ${this.dataType} data for test context ${context} in <global> storage`);
226
109
  return [];
227
110
  } catch (e) {
228
111
  // there could be no data, ignore
@@ -231,19 +114,19 @@ class DataStorage {
231
114
 
232
115
  /**
233
116
  *
234
- * @param {*} testId
117
+ * @param {*} context
235
118
  * @returns array of data (any type)
236
119
  */
237
- #getDataFromFile(testId) {
120
+ #getDataFromFile(context) {
238
121
  try {
239
- const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
122
+ const filepath = join(this.dataDirPath, `${this.dataType}_${context}`);
240
123
  if (fs.existsSync(filepath)) {
241
124
  const testDataAsText = fs.readFileSync(filepath, 'utf-8');
242
- if (testDataAsText) debug(`"${this.dataType}" data for test id ${testId}:`, testDataAsText);
125
+ if (testDataAsText) debug(`"${this.dataType}" data for test id ${context}:`, testDataAsText);
243
126
  const testDataArr = testDataAsText?.split(os.EOL) || [];
244
127
  return testDataArr;
245
128
  }
246
- // debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
129
+ // debug(`No ${this.dataType} data for test ${context} in <file> storage`);
247
130
  return [];
248
131
  } catch (e) {
249
132
  // there could be no data, ignore
@@ -254,23 +137,23 @@ class DataStorage {
254
137
  /**
255
138
  * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
256
139
  * @param {*} data
257
- * @param {*} testId
140
+ * @param {*} context
258
141
  */
259
- #putDataToGlobalVar(data, testId) {
260
- debug(`Saving data to global variable for test ${testId}:`, data);
142
+ #putDataToGlobalVar(data, context) {
143
+ debug('Saving data to global variable for ', context, ':', data);
261
144
  if (!global.testomatioDataStore) global.testomatioDataStore = {};
262
145
  if (!global.testomatioDataStore?.[this.dataType]) global.testomatioDataStore[this.dataType] = {};
263
146
 
264
- if (!global.testomatioDataStore?.[this.dataType][testId]) global.testomatioDataStore[this.dataType][testId] = [];
265
- global.testomatioDataStore[this.dataType][testId].push(data);
147
+ if (!global.testomatioDataStore?.[this.dataType][context]) global.testomatioDataStore[this.dataType][context] = [];
148
+ global.testomatioDataStore[this.dataType][context].push(data);
266
149
  }
267
150
 
268
- #putDataToFile(data, testId) {
151
+ #putDataToFile(data, context) {
269
152
  if (typeof data !== 'string') data = JSON.stringify(data);
270
- const filename = `${this.dataType}_${testId}`;
153
+ const filename = `${this.dataType}_${context}`;
271
154
  const filepath = join(this.dataDirPath, filename);
272
155
  if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
273
- debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
156
+ debug('Saving data to file for', context, 'to', filepath, ':\n', data, '\n');
274
157
 
275
158
  // append new line if file already exists (in this case its definitely includes some data)
276
159
  if (fs.existsSync(filepath)) {
@@ -285,23 +168,3 @@ module.exports = DataStorage;
285
168
 
286
169
  // TODO: consider using fs promises instead of writeSync/appendFileSync to
287
170
  // prevent blocking and improve performance (probably queue usage will be required)
288
-
289
- // TODO: rewrite client - everything regarding storing artifacts
290
- // TODO: try to define adapter inside client
291
- // TODO: ability to intercept multiple loggers (upd: no need)
292
-
293
- /* does not work for Playwright
294
- try {
295
- const { test } = require('@playwright/test');
296
- test.beforeEach(async ({}, testInfo) => {
297
- global.testTitle = testInfo.title;
298
- global.testomatioRunningEnvironment = 'playwright';
299
- });
300
- } catch (e) {
301
- // ignore
302
- }
303
- // Playwright storage works fine only when running tests from a single file;
304
- // otherwise it is not possible to define test id/title, it is overwritten
305
- TODO: the only way implementation for Pw for now is to pass testInfo inside test
306
- // (and process this testInfo inside "put" function)
307
- */
@@ -2,23 +2,39 @@ const debug = require('debug')('@testomatio/reporter:key-value-storage');
2
2
  const DataStorage = require('./data-storage');
3
3
 
4
4
  class KeyValueStorage {
5
+ static #instance;
6
+
7
+ #context;
8
+
5
9
  constructor() {
6
10
  this.dataStorage = new DataStorage('keyvalue');
11
+ }
7
12
 
8
- // singleton
9
- if (!KeyValueStorage.instance) {
10
- KeyValueStorage.instance = this;
13
+ /**
14
+ *
15
+ * @returns {KeyValueStorage}
16
+ */
17
+ static getInstance() {
18
+ if (!this.#instance) {
19
+ this.#instance = new KeyValueStorage();
11
20
  }
21
+ return this.#instance;
22
+ }
23
+
24
+ /**
25
+ * @param {string} context - suite title + test title
26
+ */
27
+ setContext(context) {
28
+ this.#context = context;
12
29
  }
13
30
 
14
31
  /**
15
32
  * Stores key-value pair and passes it to reporter
16
33
  * @param {{key: string}} keyValue - key-value pair(s) as object
17
- * @param {*} context testId or test title
18
34
  */
19
- put(keyValue, context = null) {
35
+ put(keyValue) {
20
36
  if (!keyValue) return;
21
- this.dataStorage.putData(keyValue, context);
37
+ this.dataStorage.putData(keyValue, this.#context);
22
38
  }
23
39
 
24
40
  #isKeyValueObject(smth) {
@@ -30,7 +46,8 @@ class KeyValueStorage {
30
46
  * @param {*} context testId or test context from test runner
31
47
  * @returns {Object} key-values pairs as object, e.g. {priority: 'high', browser: 'chrome'}
32
48
  */
33
- get(context) {
49
+ get(context = null) {
50
+ context = context || this.#context;
34
51
  if (!context) return null;
35
52
 
36
53
  const keyValuesList = this.dataStorage.getData(context);
@@ -53,6 +70,4 @@ class KeyValueStorage {
53
70
  }
54
71
  }
55
72
 
56
- KeyValueStorage.instance = null;
57
-
58
- module.exports = new KeyValueStorage();
73
+ module.exports.keyValueStorage = KeyValueStorage.getInstance();
@@ -26,18 +26,45 @@ class Logger {
26
26
  // set default logger to be used in log, warn, error, etc methods
27
27
  #originalUserLogger = { ...console };
28
28
 
29
- #dataStorage = new DataStorage('log');
29
+ #dataStorage;
30
+
31
+ #context = null;
32
+
33
+ static #instance;
34
+
35
+ /**
36
+ *
37
+ * @returns {Logger}
38
+ */
39
+ static getInstance() {
40
+ if (!this.#instance) {
41
+ this.#instance = new Logger();
42
+ }
43
+ return this.#instance;
44
+ }
30
45
 
31
46
  logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
32
47
 
33
48
  constructor() {
49
+ this.#dataStorage = new DataStorage('log');
50
+
34
51
  // intercept console by default
35
52
  this.intercept(console);
53
+ }
36
54
 
37
- // singleton
38
- if (!Logger.instance) {
39
- Logger.instance = this;
40
- }
55
+ /**
56
+ * @param { 'file' | 'global' } storageType
57
+ */
58
+ setStorageType(storageType) {
59
+ this.isFileStorage = storageType === 'file';
60
+ this.#dataStorage.isFileStorage = this.isFileStorage;
61
+ }
62
+
63
+ /**
64
+ * @param {string} context - suite title + test title
65
+ */
66
+ setContext(context) {
67
+ this.#context = context;
41
68
  }
42
69
 
43
70
  /**
@@ -55,17 +82,17 @@ class Logger {
55
82
  }
56
83
  }
57
84
  logs = chalk.blue(`> ${logs}`);
58
- this.#dataStorage.putData(logs);
85
+ this.#dataStorage.putData(logs, this.#context);
59
86
  }
60
87
 
61
88
  /**
62
89
  *
63
- * @param {*} context testId or test context from test runner
64
- * @returns
90
+ * @param {string} context testId or test context from test runner
91
+ * @returns {string[]}
65
92
  */
66
93
  getLogs(context) {
67
94
  const logs = this.#dataStorage.getData(context);
68
- return logs || '';
95
+ return logs;
69
96
  }
70
97
 
71
98
  #stringifyLogs(...args) {
@@ -124,7 +151,7 @@ class Logger {
124
151
  logs = this.#stringifyLogs(strings, ...args);
125
152
  }
126
153
  this.#originalUserLogger.log(logs);
127
- this.#dataStorage.putData(logs);
154
+ this.#dataStorage.putData(logs, this.#context);
128
155
  }
129
156
 
130
157
  /**
@@ -140,8 +167,12 @@ class Logger {
140
167
  if (severity < LEVELS[this.logLevel]?.severity) return;
141
168
 
142
169
  const logs = this.#stringifyLogs(...argsArray);
170
+
171
+ // skip logs from testomatio reporter itself
172
+ if (logs.includes('[TESTOMATIO]')) return;
173
+
143
174
  const colorizedLogs = chalk[LEVELS[level].color](logs);
144
- this.#dataStorage.putData(colorizedLogs);
175
+ this.#dataStorage.putData(colorizedLogs, this.#context);
145
176
  try {
146
177
  // level.toLowerCase() represents method name (log, warn, error, etc)
147
178
  this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
@@ -239,11 +270,7 @@ class Logger {
239
270
  }
240
271
  }
241
272
 
242
- Logger.instance = null;
243
-
244
- const logger = new Logger();
245
-
246
- module.exports = logger;
273
+ module.exports.logger = Logger.getInstance();
247
274
 
248
275
  // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
249
276
  // upd: did not face such loggers, but still could be useful
@@ -288,3 +315,6 @@ Finally, in the test it will look like:
288
315
 
289
316
  Parallelization in Cypress is only available if using Cypress Dashboard??
290
317
  */
318
+
319
+ // TODO: add time to logs
320
+ // TODO: add logger name to logs?
@@ -290,19 +290,6 @@ function removeColorCodes(input) {
290
290
  return input.replace(/\x1b\[[0-9;]*m/g, '');
291
291
  }
292
292
 
293
- const jestHelpers = {
294
- getIdOfCurrentlyRunningTest: () => {
295
- if (!process.env.JEST_WORKER_ID) return null;
296
- try {
297
- // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
298
- // eslint-disable-next-line no-undef
299
- if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
300
- } catch (e) {
301
- return null;
302
- }
303
- },
304
- };
305
-
306
293
  module.exports = {
307
294
  isSameTest,
308
295
  fetchSourceCode,
@@ -312,7 +299,6 @@ module.exports = {
312
299
  fetchFilesFromStackTrace,
313
300
  fileSystem,
314
301
  getCurrentDateTime,
315
- jestHelpers,
316
302
  specificTestInfo,
317
303
  isValidUrl,
318
304
  ansiRegExp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.1.0-beta.mocha-create.8",
3
+ "version": "1.1.1-beta-codecept-logger",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",