codeceptjs 4.0.0-beta.3 → 4.0.0-beta.5

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.
Files changed (155) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +141 -86
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/dryRun.js +30 -35
  17. package/lib/command/generate.js +10 -14
  18. package/lib/command/gherkin/snippets.js +75 -73
  19. package/lib/command/gherkin/steps.js +1 -1
  20. package/lib/command/info.js +42 -8
  21. package/lib/command/init.js +13 -12
  22. package/lib/command/interactive.js +10 -2
  23. package/lib/command/list.js +1 -1
  24. package/lib/command/run-multiple/chunk.js +48 -45
  25. package/lib/command/run-multiple.js +12 -35
  26. package/lib/command/run-workers.js +21 -58
  27. package/lib/command/utils.js +5 -6
  28. package/lib/command/workers/runTests.js +263 -222
  29. package/lib/container.js +386 -238
  30. package/lib/data/context.js +10 -13
  31. package/lib/data/dataScenarioConfig.js +8 -8
  32. package/lib/data/dataTableArgument.js +6 -6
  33. package/lib/data/table.js +5 -11
  34. package/lib/effects.js +223 -0
  35. package/lib/element/WebElement.js +327 -0
  36. package/lib/els.js +158 -0
  37. package/lib/event.js +21 -17
  38. package/lib/heal.js +88 -80
  39. package/lib/helper/AI.js +2 -1
  40. package/lib/helper/ApiDataFactory.js +4 -7
  41. package/lib/helper/Appium.js +50 -57
  42. package/lib/helper/FileSystem.js +3 -3
  43. package/lib/helper/GraphQLDataFactory.js +4 -4
  44. package/lib/helper/JSONResponse.js +75 -37
  45. package/lib/helper/Mochawesome.js +31 -9
  46. package/lib/helper/Nightmare.js +37 -58
  47. package/lib/helper/Playwright.js +267 -272
  48. package/lib/helper/Protractor.js +56 -87
  49. package/lib/helper/Puppeteer.js +247 -264
  50. package/lib/helper/REST.js +29 -17
  51. package/lib/helper/TestCafe.js +22 -47
  52. package/lib/helper/WebDriver.js +157 -368
  53. package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
  54. package/lib/helper/extras/Popup.js +22 -22
  55. package/lib/helper/network/utils.js +1 -1
  56. package/lib/helper/testcafe/testcafe-utils.js +27 -28
  57. package/lib/listener/emptyRun.js +55 -0
  58. package/lib/listener/exit.js +7 -10
  59. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  60. package/lib/listener/globalTimeout.js +165 -0
  61. package/lib/listener/helpers.js +15 -15
  62. package/lib/listener/mocha.js +1 -1
  63. package/lib/listener/result.js +12 -0
  64. package/lib/listener/retryEnhancer.js +85 -0
  65. package/lib/listener/steps.js +32 -18
  66. package/lib/listener/store.js +20 -0
  67. package/lib/locator.js +1 -1
  68. package/lib/mocha/asyncWrapper.js +231 -0
  69. package/lib/{interfaces → mocha}/bdd.js +3 -3
  70. package/lib/mocha/cli.js +308 -0
  71. package/lib/mocha/factory.js +104 -0
  72. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  73. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  74. package/lib/mocha/hooks.js +112 -0
  75. package/lib/mocha/index.js +12 -0
  76. package/lib/mocha/inject.js +29 -0
  77. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  78. package/lib/mocha/suite.js +82 -0
  79. package/lib/mocha/test.js +181 -0
  80. package/lib/mocha/types.d.ts +42 -0
  81. package/lib/mocha/ui.js +232 -0
  82. package/lib/output.js +93 -65
  83. package/lib/pause.js +160 -138
  84. package/lib/plugin/analyze.js +396 -0
  85. package/lib/plugin/auth.js +435 -0
  86. package/lib/plugin/autoDelay.js +8 -8
  87. package/lib/plugin/autoLogin.js +3 -338
  88. package/lib/plugin/commentStep.js +6 -1
  89. package/lib/plugin/coverage.js +10 -22
  90. package/lib/plugin/customLocator.js +3 -3
  91. package/lib/plugin/customReporter.js +52 -0
  92. package/lib/plugin/eachElement.js +1 -1
  93. package/lib/plugin/fakerTransform.js +1 -1
  94. package/lib/plugin/heal.js +36 -9
  95. package/lib/plugin/htmlReporter.js +1947 -0
  96. package/lib/plugin/pageInfo.js +140 -0
  97. package/lib/plugin/retryFailedStep.js +17 -18
  98. package/lib/plugin/retryTo.js +2 -113
  99. package/lib/plugin/screenshotOnFail.js +17 -58
  100. package/lib/plugin/selenoid.js +15 -35
  101. package/lib/plugin/standardActingHelpers.js +4 -1
  102. package/lib/plugin/stepByStepReport.js +56 -17
  103. package/lib/plugin/stepTimeout.js +5 -12
  104. package/lib/plugin/subtitles.js +4 -4
  105. package/lib/plugin/tryTo.js +3 -102
  106. package/lib/plugin/wdio.js +8 -10
  107. package/lib/recorder.js +155 -124
  108. package/lib/rerun.js +43 -42
  109. package/lib/result.js +161 -0
  110. package/lib/secret.js +1 -2
  111. package/lib/step/base.js +239 -0
  112. package/lib/step/comment.js +10 -0
  113. package/lib/step/config.js +50 -0
  114. package/lib/step/func.js +46 -0
  115. package/lib/step/helper.js +50 -0
  116. package/lib/step/meta.js +99 -0
  117. package/lib/step/record.js +74 -0
  118. package/lib/step/retry.js +11 -0
  119. package/lib/step/section.js +55 -0
  120. package/lib/step.js +21 -332
  121. package/lib/steps.js +50 -0
  122. package/lib/store.js +37 -5
  123. package/lib/template/heal.js +2 -11
  124. package/lib/test-server.js +323 -0
  125. package/lib/timeout.js +66 -0
  126. package/lib/utils.js +351 -218
  127. package/lib/within.js +75 -55
  128. package/lib/workerStorage.js +2 -1
  129. package/lib/workers.js +386 -277
  130. package/package.json +81 -75
  131. package/translations/de-DE.js +5 -3
  132. package/translations/fr-FR.js +5 -4
  133. package/translations/index.js +1 -0
  134. package/translations/it-IT.js +4 -3
  135. package/translations/ja-JP.js +4 -3
  136. package/translations/nl-NL.js +76 -0
  137. package/translations/pl-PL.js +4 -3
  138. package/translations/pt-BR.js +4 -3
  139. package/translations/ru-RU.js +4 -3
  140. package/translations/utils.js +9 -0
  141. package/translations/zh-CN.js +4 -3
  142. package/translations/zh-TW.js +4 -3
  143. package/typings/index.d.ts +197 -187
  144. package/typings/promiseBasedTypes.d.ts +53 -903
  145. package/typings/types.d.ts +372 -1042
  146. package/lib/cli.js +0 -257
  147. package/lib/helper/ExpectHelper.js +0 -391
  148. package/lib/helper/MockServer.js +0 -221
  149. package/lib/helper/SoftExpectHelper.js +0 -381
  150. package/lib/listener/artifacts.js +0 -19
  151. package/lib/listener/timeout.js +0 -109
  152. package/lib/mochaFactory.js +0 -113
  153. package/lib/plugin/debugErrors.js +0 -67
  154. package/lib/scenario.js +0 -224
  155. package/lib/ui.js +0 -236
package/lib/utils.js CHANGED
@@ -1,139 +1,131 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const getFunctionArguments = require('fn-args');
5
- const deepClone = require('lodash.clonedeep');
6
- const { convertColorToRGBA, isColorProperty } = require('./colorUtils');
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+ const path = require('path')
4
+ const chalk = require('chalk')
5
+ const getFunctionArguments = require('fn-args')
6
+ const deepClone = require('lodash.clonedeep')
7
+ const { convertColorToRGBA, isColorProperty } = require('./colorUtils')
8
+ const Fuse = require('fuse.js')
9
+ const { spawnSync } = require('child_process')
7
10
 
8
11
  function deepMerge(target, source) {
9
- const merge = require('lodash.merge');
10
- return merge(target, source);
12
+ const merge = require('lodash.merge')
13
+ return merge(target, source)
11
14
  }
12
15
 
13
- module.exports.genTestId = (test) => {
14
- return require('crypto').createHash('sha256').update(test.fullTitle()).digest('base64')
15
- .slice(0, -2);
16
- };
16
+ module.exports.genTestId = test => {
17
+ return this.clearString(require('crypto').createHash('sha256').update(test.fullTitle()).digest('base64').slice(0, -2))
18
+ }
17
19
 
18
- module.exports.deepMerge = deepMerge;
20
+ module.exports.deepMerge = deepMerge
19
21
 
20
- module.exports.deepClone = deepClone;
22
+ module.exports.deepClone = deepClone
21
23
 
22
24
  module.exports.isGenerator = function (fn) {
23
- return fn.constructor.name === 'GeneratorFunction';
24
- };
25
+ return fn.constructor.name === 'GeneratorFunction'
26
+ }
25
27
 
26
- const isFunction = module.exports.isFunction = function (fn) {
27
- return typeof fn === 'function';
28
- };
28
+ const isFunction = (module.exports.isFunction = function (fn) {
29
+ return typeof fn === 'function'
30
+ })
29
31
 
30
- const isAsyncFunction = module.exports.isAsyncFunction = function (fn) {
31
- if (!fn) return false;
32
- return fn[Symbol.toStringTag] === 'AsyncFunction';
33
- };
32
+ const isAsyncFunction = (module.exports.isAsyncFunction = function (fn) {
33
+ if (!fn) return false
34
+ return fn[Symbol.toStringTag] === 'AsyncFunction'
35
+ })
34
36
 
35
37
  module.exports.fileExists = function (filePath) {
36
- return fs.existsSync(filePath);
37
- };
38
+ return fs.existsSync(filePath)
39
+ }
38
40
 
39
41
  module.exports.isFile = function (filePath) {
40
- let filestat;
42
+ let filestat
41
43
  try {
42
- filestat = fs.statSync(filePath);
44
+ filestat = fs.statSync(filePath)
43
45
  } catch (err) {
44
- if (err.code === 'ENOENT') return false;
46
+ if (err.code === 'ENOENT') return false
45
47
  }
46
- if (!filestat) return false;
47
- return filestat.isFile();
48
- };
48
+ if (!filestat) return false
49
+ return filestat.isFile()
50
+ }
49
51
 
50
52
  module.exports.getParamNames = function (fn) {
51
- if (fn.isSinonProxy) return [];
52
- return getFunctionArguments(fn);
53
- };
53
+ if (fn.isSinonProxy) return []
54
+ return getFunctionArguments(fn)
55
+ }
54
56
 
55
57
  module.exports.installedLocally = function () {
56
- return path.resolve(`${__dirname}/../`).indexOf(process.cwd()) === 0;
57
- };
58
+ return path.resolve(`${__dirname}/../`).indexOf(process.cwd()) === 0
59
+ }
58
60
 
59
61
  module.exports.methodsOfObject = function (obj, className) {
60
- const methods = [];
61
-
62
- const standard = [
63
- 'constructor',
64
- 'toString',
65
- 'toLocaleString',
66
- 'valueOf',
67
- 'hasOwnProperty',
68
- 'bind',
69
- 'apply',
70
- 'call',
71
- 'isPrototypeOf',
72
- 'propertyIsEnumerable',
73
- ];
62
+ const methods = []
63
+
64
+ const standard = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'bind', 'apply', 'call', 'isPrototypeOf', 'propertyIsEnumerable']
74
65
 
75
66
  function pushToMethods(prop) {
76
67
  try {
77
- if (!isFunction(obj[prop]) && !isAsyncFunction(obj[prop])) return;
78
- } catch (err) { // can't access property
79
- return;
68
+ if (!isFunction(obj[prop]) && !isAsyncFunction(obj[prop])) return
69
+ } catch (err) {
70
+ // can't access property
71
+ return
80
72
  }
81
- if (standard.indexOf(prop) >= 0) return;
82
- if (prop.indexOf('_') === 0) return;
83
- methods.push(prop);
73
+ if (standard.indexOf(prop) >= 0) return
74
+ if (prop.indexOf('_') === 0) return
75
+ methods.push(prop)
84
76
  }
85
77
 
86
78
  while (obj.constructor.name !== className) {
87
- Object.getOwnPropertyNames(obj).forEach(pushToMethods);
88
- obj = Object.getPrototypeOf(obj);
79
+ Object.getOwnPropertyNames(obj).forEach(pushToMethods)
80
+ obj = Object.getPrototypeOf(obj)
89
81
 
90
- if (!obj || !obj.constructor) break;
82
+ if (!obj || !obj.constructor) break
91
83
  }
92
- return methods;
93
- };
84
+ return methods
85
+ }
94
86
 
95
87
  module.exports.template = function (template, data) {
96
88
  return template.replace(/{{([^{}]*)}}/g, (a, b) => {
97
- const r = data[b];
98
- if (r === undefined) return '';
99
- return r.toString();
100
- });
101
- };
89
+ const r = data[b]
90
+ if (r === undefined) return ''
91
+ return r.toString()
92
+ })
93
+ }
102
94
 
103
95
  /**
104
96
  * Make first char uppercase.
105
97
  * @param {string} str
106
- * @returns {string}
98
+ * @returns {string | undefined}
107
99
  */
108
100
  module.exports.ucfirst = function (str) {
109
- return str.charAt(0).toUpperCase() + str.substr(1);
110
- };
101
+ if (str) return str.charAt(0).toUpperCase() + str.substr(1)
102
+ }
111
103
 
112
104
  /**
113
105
  * Make first char lowercase.
114
106
  * @param {string} str
115
- * @returns {string}
107
+ * @returns {string | undefined}
116
108
  */
117
109
  module.exports.lcfirst = function (str) {
118
- return str.charAt(0).toLowerCase() + str.substr(1);
119
- };
110
+ if (str) return str.charAt(0).toLowerCase() + str.substr(1)
111
+ }
120
112
 
121
113
  module.exports.chunkArray = function (arr, chunk) {
122
- let i;
123
- let j;
124
- const tmp = [];
114
+ let i
115
+ let j
116
+ const tmp = []
125
117
  for (i = 0, j = arr.length; i < j; i += chunk) {
126
- tmp.push(arr.slice(i, i + chunk));
118
+ tmp.push(arr.slice(i, i + chunk))
127
119
  }
128
- return tmp;
129
- };
120
+ return tmp
121
+ }
130
122
 
131
123
  module.exports.clearString = function (str) {
132
- if (!str) return '';
124
+ if (!str) return ''
133
125
  /* Replace forbidden symbols in string
134
126
  */
135
127
  if (str.endsWith('.')) {
136
- str = str.slice(0, -1);
128
+ str = str.slice(0, -1)
137
129
  }
138
130
  return str
139
131
  .replace(/ /g, '_')
@@ -146,26 +138,29 @@ module.exports.clearString = function (str) {
146
138
  .replace(/\|/g, '_')
147
139
  .replace(/\?/g, '.')
148
140
  .replace(/\*/g, '^')
149
- .replace(/'/g, '');
150
- };
141
+ .replace(/'/g, '')
142
+ }
151
143
 
152
144
  module.exports.decodeUrl = function (url) {
153
145
  /* Replace forbidden symbols in string
154
146
  */
155
- return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)));
156
- };
147
+ return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)))
148
+ }
157
149
 
158
150
  module.exports.xpathLocator = {
159
151
  /**
160
152
  * @param {string} string
161
153
  * @returns {string}
162
154
  */
163
- literal: (string) => {
155
+ literal: string => {
164
156
  if (string.indexOf("'") > -1) {
165
- string = string.split("'", -1).map(substr => `'${substr}'`).join(',"\'",');
166
- return `concat(${string})`;
157
+ string = string
158
+ .split("'", -1)
159
+ .map(substr => `'${substr}'`)
160
+ .join(',"\'",')
161
+ return `concat(${string})`
167
162
  }
168
- return `'${string}'`;
163
+ return `'${string}'`
169
164
  },
170
165
 
171
166
  /**
@@ -174,55 +169,84 @@ module.exports.xpathLocator = {
174
169
  * @returns {string}
175
170
  */
176
171
  combine: locators => locators.join(' | '),
177
- };
172
+ }
178
173
 
179
174
  module.exports.test = {
180
-
181
175
  grepLines(array, startString, endString) {
182
- let startIndex = 0;
183
- let endIndex;
176
+ let startIndex = 0
177
+ let endIndex
184
178
  array.every((elem, index) => {
185
179
  if (elem === startString) {
186
- startIndex = index;
187
- return true;
180
+ startIndex = index
181
+ return true
188
182
  }
189
183
  if (elem === endString) {
190
- endIndex = index;
191
- return false;
184
+ endIndex = index
185
+ return false
192
186
  }
193
- return true;
194
- });
195
- return array.slice(startIndex + 1, endIndex);
187
+ return true
188
+ })
189
+ return array.slice(startIndex + 1, endIndex)
196
190
  },
197
191
 
198
192
  submittedData(dataFile) {
199
193
  return function (key) {
200
194
  if (!fs.existsSync(dataFile)) {
201
- const waitTill = new Date(new Date().getTime() + 1 * 1000); // wait for one sec for file to be created
202
- while (waitTill > new Date()) {} // eslint-disable-line no-empty
195
+ // Extended timeout for CI environments to handle slower processing
196
+ const waitTime = process.env.CI ? 60 * 1000 : 2 * 1000 // 60 seconds in CI, 2 seconds otherwise
197
+ let pollInterval = 100 // Start with 100ms polling interval
198
+ const maxPollInterval = 2000 // Max 2 second intervals
199
+ const startTime = new Date().getTime()
200
+
201
+ // Synchronous polling with exponential backoff to reduce CPU usage
202
+ while (new Date().getTime() - startTime < waitTime) {
203
+ if (fs.existsSync(dataFile)) {
204
+ break
205
+ }
206
+
207
+ // Use Node.js child_process.spawnSync with platform-specific sleep commands
208
+ // This avoids busy waiting and allows other processes to run
209
+ try {
210
+ if (os.platform() === 'win32') {
211
+ // Windows: use ping with precise timing (ping waits exactly the specified ms)
212
+ spawnSync('ping', ['-n', '1', '-w', pollInterval.toString(), '127.0.0.1'], { stdio: 'ignore' })
213
+ } else {
214
+ // Unix/Linux/macOS: use sleep with fractional seconds
215
+ spawnSync('sleep', [(pollInterval / 1000).toString()], { stdio: 'ignore' })
216
+ }
217
+ } catch (err) {
218
+ // If system commands fail, use a simple busy wait with minimal CPU usage
219
+ const end = new Date().getTime() + pollInterval
220
+ while (new Date().getTime() < end) {
221
+ // No-op loop - much lighter than previous approaches
222
+ }
223
+ }
224
+
225
+ // Exponential backoff: gradually increase polling interval to reduce resource usage
226
+ pollInterval = Math.min(pollInterval * 1.2, maxPollInterval)
227
+ }
203
228
  }
204
229
  if (!fs.existsSync(dataFile)) {
205
- throw new Error('Data file was not created in time');
230
+ throw new Error('Data file was not created in time')
206
231
  }
207
- const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
232
+ const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'))
208
233
  if (key) {
209
- return data.form[key];
234
+ return data.form[key]
210
235
  }
211
- return data;
212
- };
236
+ return data
237
+ }
213
238
  },
214
-
215
- };
239
+ }
216
240
 
217
241
  function toCamelCase(name) {
218
242
  if (typeof name !== 'string') {
219
- return name;
243
+ return name
220
244
  }
221
245
  return name.replace(/-(\w)/gi, (_word, letter) => {
222
- return letter.toUpperCase();
223
- });
246
+ return letter.toUpperCase()
247
+ })
224
248
  }
225
- module.exports.toCamelCase = toCamelCase;
249
+ module.exports.toCamelCase = toCamelCase
226
250
 
227
251
  function convertFontWeightToNumber(name) {
228
252
  const fontWeightPatterns = [
@@ -235,106 +259,110 @@ function convertFontWeightToNumber(name) {
235
259
  { num: 700, pattern: /^Bold$/i },
236
260
  { num: 800, pattern: /^(Extra|Ultra)-?bold$/i },
237
261
  { num: 900, pattern: /^(Black|Heavy)$/i },
238
- ];
262
+ ]
239
263
 
240
264
  if (/^[1-9]00$/.test(name)) {
241
- return Number(name);
265
+ return Number(name)
242
266
  }
243
267
 
244
- const matches = fontWeightPatterns.filter(fontWeight => fontWeight.pattern.test(name));
268
+ const matches = fontWeightPatterns.filter(fontWeight => fontWeight.pattern.test(name))
245
269
 
246
270
  if (matches.length) {
247
- return String(matches[0].num);
271
+ return String(matches[0].num)
248
272
  }
249
- return name;
273
+ return name
250
274
  }
251
275
 
252
276
  function isFontWeightProperty(prop) {
253
- return prop === 'fontWeight';
277
+ return prop === 'fontWeight'
254
278
  }
255
279
 
256
280
  module.exports.convertCssPropertiesToCamelCase = function (props) {
257
- const output = {};
258
- Object.keys(props).forEach((key) => {
259
- const keyCamel = toCamelCase(key);
281
+ const output = {}
282
+ Object.keys(props).forEach(key => {
283
+ const keyCamel = toCamelCase(key)
260
284
 
261
285
  if (isFontWeightProperty(keyCamel)) {
262
- output[keyCamel] = convertFontWeightToNumber(props[key]);
286
+ output[keyCamel] = convertFontWeightToNumber(props[key])
263
287
  } else if (isColorProperty(keyCamel)) {
264
- output[keyCamel] = convertColorToRGBA(props[key]);
288
+ output[keyCamel] = convertColorToRGBA(props[key])
265
289
  } else {
266
- output[keyCamel] = props[key];
290
+ output[keyCamel] = props[key]
267
291
  }
268
- });
269
- return output;
270
- };
292
+ })
293
+ return output
294
+ }
271
295
 
272
296
  module.exports.deleteDir = function (dir_path) {
273
297
  if (fs.existsSync(dir_path)) {
274
298
  fs.readdirSync(dir_path).forEach(function (entry) {
275
- const entry_path = path.join(dir_path, entry);
299
+ const entry_path = path.join(dir_path, entry)
276
300
  if (fs.lstatSync(entry_path).isDirectory()) {
277
- this.deleteDir(entry_path);
301
+ this.deleteDir(entry_path)
278
302
  } else {
279
- fs.unlinkSync(entry_path);
303
+ fs.unlinkSync(entry_path)
280
304
  }
281
- });
282
- fs.rmdirSync(dir_path);
305
+ })
306
+ fs.rmdirSync(dir_path)
283
307
  }
284
- };
308
+ }
285
309
 
286
310
  /**
287
311
  * Returns absolute filename to save screenshot.
288
312
  * @param fileName {string} - filename.
289
313
  */
290
314
  module.exports.screenshotOutputFolder = function (fileName) {
291
- const fileSep = path.sep;
315
+ const fileSep = path.sep
292
316
 
293
317
  if (!fileName.includes(fileSep) || fileName.includes('record_')) {
294
- return path.resolve(global.output_dir, fileName);
318
+ return path.resolve(global.output_dir, fileName)
295
319
  }
296
- return path.resolve(global.codecept_dir, fileName);
297
- };
320
+ return path.resolve(global.codecept_dir, fileName)
321
+ }
322
+
323
+ module.exports.relativeDir = function (fileName) {
324
+ return fileName.replace(global.codecept_dir, '').replace(/^\//, '')
325
+ }
298
326
 
299
327
  module.exports.beautify = function (code) {
300
- const format = require('js-beautify').js;
301
- return format(code, { indent_size: 2, space_in_empty_paren: true });
302
- };
328
+ const format = require('js-beautify').js
329
+ return format(code, { indent_size: 2, space_in_empty_paren: true })
330
+ }
303
331
 
304
332
  function shouldAppendBaseUrl(url) {
305
- return !/^\w+\:\/\//.test(url);
333
+ return !/^\w+\:\/\//.test(url)
306
334
  }
307
335
 
308
336
  function trimUrl(url) {
309
- const firstChar = url.substr(1);
337
+ const firstChar = url.substr(1)
310
338
  if (firstChar === '/') {
311
- url = url.slice(1);
339
+ url = url.slice(1)
312
340
  }
313
- return url;
341
+ return url
314
342
  }
315
343
 
316
344
  function joinUrl(baseUrl, url) {
317
- return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url;
345
+ return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url
318
346
  }
319
347
 
320
348
  module.exports.appendBaseUrl = function (baseUrl = '', oneOrMoreUrls) {
321
349
  if (typeof baseUrl !== 'string') {
322
- throw new Error(`Invalid value for baseUrl: ${baseUrl}`);
350
+ throw new Error(`Invalid value for baseUrl: ${baseUrl}`)
323
351
  }
324
352
  if (!(typeof oneOrMoreUrls === 'string' || Array.isArray(oneOrMoreUrls))) {
325
- throw new Error(`Expected type of Urls is 'string' or 'array', Found '${typeof oneOrMoreUrls}'.`);
353
+ throw new Error(`Expected type of Urls is 'string' or 'array', Found '${typeof oneOrMoreUrls}'.`)
326
354
  }
327
355
  // Remove '/' if it's at the end of baseUrl
328
- const lastChar = baseUrl.substr(-1);
356
+ const lastChar = baseUrl.substr(-1)
329
357
  if (lastChar === '/') {
330
- baseUrl = baseUrl.slice(0, -1);
358
+ baseUrl = baseUrl.slice(0, -1)
331
359
  }
332
360
 
333
361
  if (!Array.isArray(oneOrMoreUrls)) {
334
- return joinUrl(baseUrl, oneOrMoreUrls);
362
+ return joinUrl(baseUrl, oneOrMoreUrls)
335
363
  }
336
- return oneOrMoreUrls.map(url => joinUrl(baseUrl, url));
337
- };
364
+ return oneOrMoreUrls.map(url => joinUrl(baseUrl, url))
365
+ }
338
366
 
339
367
  /**
340
368
  * Recursively search key in object and replace it's value.
@@ -344,56 +372,53 @@ module.exports.appendBaseUrl = function (baseUrl = '', oneOrMoreUrls) {
344
372
  * @param {*} value value to set for key
345
373
  */
346
374
  module.exports.replaceValueDeep = function replaceValueDeep(obj, key, value) {
347
- if (!obj) return;
375
+ if (!obj) return
348
376
 
349
377
  if (obj instanceof Array) {
350
378
  for (const i in obj) {
351
- replaceValueDeep(obj[i], key, value);
379
+ replaceValueDeep(obj[i], key, value)
352
380
  }
353
381
  }
354
382
 
355
383
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
356
- obj[key] = value;
384
+ obj[key] = value
357
385
  }
358
386
 
359
387
  if (typeof obj === 'object' && obj !== null) {
360
- const children = Object.values(obj);
388
+ const children = Object.values(obj)
361
389
  for (const child of children) {
362
- replaceValueDeep(child, key, value);
390
+ replaceValueDeep(child, key, value)
363
391
  }
364
392
  }
365
- return obj;
366
- };
393
+ return obj
394
+ }
367
395
 
368
396
  module.exports.ansiRegExp = function ({ onlyFirst = false } = {}) {
369
- const pattern = [
370
- '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
371
- '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
372
- ].join('|');
397
+ const pattern = ['[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'].join('|')
373
398
 
374
- return new RegExp(pattern, onlyFirst ? undefined : 'g');
375
- };
399
+ return new RegExp(pattern, onlyFirst ? undefined : 'g')
400
+ }
376
401
 
377
402
  module.exports.tryOrDefault = function (fn, defaultValue) {
378
403
  try {
379
- return fn();
404
+ return fn()
380
405
  } catch (_) {
381
- return defaultValue;
406
+ return defaultValue
382
407
  }
383
- };
408
+ }
384
409
 
385
410
  function normalizeKeyReplacer(match, prefix, key, suffix, offset, string) {
386
411
  if (typeof key !== 'string') {
387
- return string;
412
+ return string
388
413
  }
389
- const normalizedKey = key.charAt(0).toUpperCase() + key.substr(1).toLowerCase();
390
- let position = '';
414
+ const normalizedKey = key.charAt(0).toUpperCase() + key.substr(1).toLowerCase()
415
+ let position = ''
391
416
  if (typeof prefix === 'string') {
392
- position = prefix;
417
+ position = prefix
393
418
  } else if (typeof suffix === 'string') {
394
- position = suffix;
419
+ position = suffix
395
420
  }
396
- return normalizedKey + position.charAt(0).toUpperCase() + position.substr(1).toLowerCase();
421
+ return normalizedKey + position.charAt(0).toUpperCase() + position.substr(1).toLowerCase()
397
422
  }
398
423
 
399
424
  /**
@@ -403,76 +428,184 @@ function normalizeKeyReplacer(match, prefix, key, suffix, offset, string) {
403
428
  */
404
429
  module.exports.getNormalizedKeyAttributeValue = function (key) {
405
430
  // Use operation modifier key based on operating system
406
- key = key.replace(/(Ctrl|Control|Cmd|Command)[ _]?Or[ _]?(Ctrl|Control|Cmd|Command)/i, os.platform() === 'darwin' ? 'Meta' : 'Control');
431
+ key = key.replace(/(Ctrl|Control|Cmd|Command)[ _]?Or[ _]?(Ctrl|Control|Cmd|Command)/i, os.platform() === 'darwin' ? 'Meta' : 'Control')
407
432
  // Selection of keys (https://www.w3.org/TR/uievents-key/#named-key-attribute-values)
408
433
  // which can be written in various ways and should be normalized.
409
434
  // For example 'LEFT ALT', 'ALT_Left', 'alt left' or 'LeftAlt' will be normalized as 'AltLeft'.
410
- key = key.replace(/^\s*(?:(Down|Left|Right|Up)[ _]?)?(Arrow|Alt|Ctrl|Control|Cmd|Command|Meta|Option|OS|Page|Shift|Super)(?:[ _]?(Down|Left|Right|Up|Gr(?:aph)?))?\s*$/i, normalizeKeyReplacer);
435
+ key = key.replace(/^\s*(?:(Down|Left|Right|Up)[ _]?)?(Arrow|Alt|Ctrl|Control|Cmd|Command|Meta|Option|OS|Page|Shift|Super)(?:[ _]?(Down|Left|Right|Up|Gr(?:aph)?))?\s*$/i, normalizeKeyReplacer)
411
436
  // Map alias to corresponding key value
412
- key = key.replace(/^(Add|Divide|Decimal|Multiply|Subtract)$/, 'Numpad$1');
413
- key = key.replace(/^AltGr$/, 'AltGraph');
414
- key = key.replace(/^(Cmd|Command|Os|Super)/, 'Meta');
415
- key = key.replace('Ctrl', 'Control');
416
- key = key.replace('Option', 'Alt');
417
- key = key.replace(/^(NumpadComma|Separator)$/, 'Comma');
418
- return key;
419
- };
420
-
421
- const modifierKeys = [
422
- 'Alt', 'AltGraph', 'AltLeft', 'AltRight',
423
- 'Control', 'ControlLeft', 'ControlRight',
424
- 'Meta', 'MetaLeft', 'MetaRight',
425
- 'Shift', 'ShiftLeft', 'ShiftRight',
426
- ];
427
-
428
- module.exports.modifierKeys = modifierKeys;
437
+ key = key.replace(/^(Add|Divide|Decimal|Multiply|Subtract)$/, 'Numpad$1')
438
+ key = key.replace(/^AltGr$/, 'AltGraph')
439
+ key = key.replace(/^(Cmd|Command|Os|Super)/, 'Meta')
440
+ key = key.replace('Ctrl', 'Control')
441
+ key = key.replace('Option', 'Alt')
442
+ key = key.replace(/^(NumpadComma|Separator)$/, 'Comma')
443
+ return key
444
+ }
445
+
446
+ const modifierKeys = ['Alt', 'AltGraph', 'AltLeft', 'AltRight', 'Control', 'ControlLeft', 'ControlRight', 'Meta', 'MetaLeft', 'MetaRight', 'Shift', 'ShiftLeft', 'ShiftRight']
447
+
448
+ module.exports.modifierKeys = modifierKeys
429
449
  module.exports.isModifierKey = function (key) {
430
- return modifierKeys.includes(key);
431
- };
450
+ return modifierKeys.includes(key)
451
+ }
432
452
 
433
453
  module.exports.requireWithFallback = function (...packages) {
434
454
  const exists = function (pkg) {
435
455
  try {
436
- require.resolve(pkg);
456
+ require.resolve(pkg)
437
457
  } catch (e) {
438
- return false;
458
+ return false
439
459
  }
440
460
 
441
- return true;
442
- };
461
+ return true
462
+ }
443
463
 
444
464
  for (const pkg of packages) {
445
465
  if (exists(pkg)) {
446
- return require(pkg);
466
+ return require(pkg)
447
467
  }
448
468
  }
449
469
 
450
- throw new Error(`Cannot find modules ${packages.join(',')}`);
451
- };
470
+ throw new Error(`Cannot find modules ${packages.join(',')}`)
471
+ }
452
472
 
453
473
  module.exports.isNotSet = function (obj) {
454
- if (obj === null) return true;
455
- if (obj === undefined) return true;
456
- return false;
457
- };
474
+ if (obj === null) return true
475
+ if (obj === undefined) return true
476
+ return false
477
+ }
458
478
 
459
- module.exports.emptyFolder = async (directoryPath) => {
460
- require('child_process').execSync(`rm -rf ${directoryPath}/*`);
461
- };
479
+ module.exports.emptyFolder = async directoryPath => {
480
+ require('child_process').execSync(`rm -rf ${directoryPath}/*`)
481
+ }
462
482
 
463
- module.exports.printObjectProperties = (obj) => {
483
+ module.exports.printObjectProperties = obj => {
464
484
  if (typeof obj !== 'object' || obj === null) {
465
- return obj;
485
+ return obj
466
486
  }
467
487
 
468
- let result = '';
488
+ let result = ''
469
489
  for (const [key, value] of Object.entries(obj)) {
470
- result += `${key}: "${value}"; `;
490
+ result += `${key}: "${value}"; `
491
+ }
492
+
493
+ return `{${result}}`
494
+ }
495
+
496
+ module.exports.normalizeSpacesInString = string => {
497
+ return string.replace(/\s+/g, ' ')
498
+ }
499
+
500
+ module.exports.humanizeFunction = function (fn) {
501
+ const fnStr = fn.toString().trim()
502
+ // Remove arrow function syntax, async, and parentheses
503
+ let simplified = fnStr
504
+ .replace(/^async\s*/, '')
505
+ .replace(/^\([^)]*\)\s*=>/, '')
506
+ .replace(/^function\s*\([^)]*\)/, '')
507
+ // Remove curly braces and any whitespace around them
508
+ .replace(/{\s*(.*)\s*}/, '$1')
509
+ // Remove return statement
510
+ .replace(/return\s+/, '')
511
+ // Remove trailing semicolon
512
+ .replace(/;$/, '')
513
+ .trim()
514
+
515
+ if (simplified.length > 100) {
516
+ simplified = simplified.slice(0, 97) + '...'
517
+ }
518
+
519
+ return simplified
520
+ }
521
+
522
+ /**
523
+ * Searches through a given data source using the Fuse.js library for fuzzy searching.
524
+ *
525
+ * @function searchWithFusejs
526
+ * @param {Array|Object} source - The data source to search through. This can be an array of objects or strings.
527
+ * @param {string} searchString - The search query string to match against the source.
528
+ * @param {Object} [opts] - Optional configuration object for Fuse.js.
529
+ * @param {boolean} [opts.includeScore=true] - Whether to include the score of the match in the results.
530
+ * @param {number} [opts.threshold=0.6] - Determines the match threshold; lower values mean stricter matching.
531
+ * @param {boolean} [opts.caseSensitive=false] - Whether the search should be case-sensitive.
532
+ * @param {number} [opts.distance=100] - Determines how far apart the search term is allowed to be from the target.
533
+ * @param {number} [opts.maxPatternLength=32] - The maximum length of the search pattern. Patterns longer than this are ignored.
534
+ * @param {boolean} [opts.ignoreLocation=false] - Whether the location of the match is ignored when scoring.
535
+ * @param {boolean} [opts.ignoreFieldNorm=false] - When true, the field's length is not considered when scoring.
536
+ * @param {Array<string>} [opts.keys=[]] - List of keys to search in the objects of the source array.
537
+ * @param {boolean} [opts.shouldSort=true] - Whether the results should be sorted by score.
538
+ * @param {string} [opts.sortFn] - A custom sorting function for sorting results.
539
+ * @param {number} [opts.minMatchCharLength=1] - The minimum number of characters that must match.
540
+ * @param {boolean} [opts.useExtendedSearch=false] - Enables extended search capabilities.
541
+ *
542
+ * @returns {Array<Object>} - An array of search results. Each result contains an item and, if `includeScore` is true, a score.
543
+ *
544
+ * @example
545
+ * const data = [
546
+ * { title: "Old Man's War", author: "John Scalzi" },
547
+ * { title: "The Lock Artist", author: "Steve Hamilton" },
548
+ * ];
549
+ *
550
+ * const options = {
551
+ * keys: ['title', 'author'],
552
+ * includeScore: true,
553
+ * threshold: 0.4,
554
+ * caseSensitive: false,
555
+ * distance: 50,
556
+ * ignoreLocation: true,
557
+ * };
558
+ *
559
+ * const results = searchWithFusejs(data, 'lock', options);
560
+ * console.log(results);
561
+ */
562
+ module.exports.searchWithFusejs = function (source, searchString, opts) {
563
+ const fuse = new Fuse(source, opts)
564
+
565
+ return fuse.search(searchString)
566
+ }
567
+
568
+ module.exports.humanizeString = function (string) {
569
+ // split strings by words, then make them all lowercase
570
+ const _result = string
571
+ .replace(/([a-z](?=[A-Z]))/g, '$1 ')
572
+ .split(' ')
573
+ .map(word => word.toLowerCase())
574
+
575
+ _result[0] = _result[0] === 'i' ? this.ucfirst(_result[0]) : _result[0]
576
+ return _result.join(' ').trim()
577
+ }
578
+
579
+ module.exports.serializeError = function (error) {
580
+ if (error) {
581
+ const { stack, uncaught, message, actual, expected } = error
582
+ return { stack, uncaught, message, actual, expected }
471
583
  }
584
+ return null
585
+ }
472
586
 
473
- return `{${result}}`;
474
- };
587
+ module.exports.base64EncodeFile = function (filePath) {
588
+ return Buffer.from(fs.readFileSync(filePath)).toString('base64')
589
+ }
475
590
 
476
- module.exports.normalizeSpacesInString = (string) => {
477
- return string.replace(/\s+/g, ' ');
478
- };
591
+ module.exports.markdownToAnsi = function (markdown) {
592
+ return (
593
+ markdown
594
+ // Headers (# Text) - make blue and bold
595
+ .replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => {
596
+ return chalk.bold.blue(`${hashes} ${text}`)
597
+ })
598
+ // Bullet points - replace with yellow bullet character
599
+ .replace(/^[-*]\s+(.+)$/gm, (_, text) => {
600
+ return `${chalk.yellow('•')} ${text}`
601
+ })
602
+ // Bold (**text**) - make bold
603
+ .replace(/\*\*(.+?)\*\*/g, (_, text) => {
604
+ return chalk.bold(text)
605
+ })
606
+ // Italic (*text*) - make italic (dim in terminals)
607
+ .replace(/\*(.+?)\*/g, (_, text) => {
608
+ return chalk.italic(text)
609
+ })
610
+ )
611
+ }