codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20

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