codeceptjs 3.5.0 → 3.5.1-2.beta.7

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 (273) hide show
  1. package/README.md +24 -25
  2. package/lib/actor.js +6 -3
  3. package/lib/ai.js +12 -3
  4. package/lib/cli.js +12 -2
  5. package/lib/codecept.js +4 -0
  6. package/lib/colorUtils.js +10 -0
  7. package/lib/command/definitions.js +2 -7
  8. package/lib/command/dryRun.js +2 -1
  9. package/lib/command/info.js +24 -0
  10. package/lib/command/init.js +51 -5
  11. package/lib/command/run-multiple/collection.js +17 -5
  12. package/lib/command/run-multiple.js +4 -2
  13. package/lib/command/run-workers.js +66 -4
  14. package/lib/command/run.js +7 -0
  15. package/lib/command/workers/runTests.js +39 -0
  16. package/lib/data/context.js +14 -6
  17. package/lib/event.js +4 -0
  18. package/lib/helper/ApiDataFactory.js +2 -1
  19. package/lib/helper/Appium.js +73 -24
  20. package/lib/helper/Expect.js +422 -0
  21. package/lib/helper/FileSystem.js +1 -1
  22. package/lib/helper/GraphQL.js +25 -0
  23. package/lib/helper/Nightmare.js +9 -4
  24. package/lib/helper/OpenAI.js +14 -10
  25. package/lib/helper/Playwright.js +1205 -288
  26. package/lib/helper/Protractor.js +11 -6
  27. package/lib/helper/Puppeteer.js +173 -61
  28. package/lib/helper/TestCafe.js +44 -9
  29. package/lib/helper/WebDriver.js +231 -82
  30. package/lib/helper/errors/ElementNotFound.js +2 -1
  31. package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
  32. package/lib/helper/scripts/blurElement.js +17 -0
  33. package/lib/helper/scripts/focusElement.js +17 -0
  34. package/lib/helper/scripts/highlightElement.js +2 -2
  35. package/lib/html.js +3 -3
  36. package/lib/interfaces/bdd.js +1 -1
  37. package/lib/interfaces/gherkin.js +37 -3
  38. package/lib/interfaces/scenarioConfig.js +1 -0
  39. package/lib/locator.js +17 -4
  40. package/lib/mochaFactory.js +2 -1
  41. package/lib/output.js +1 -1
  42. package/lib/pause.js +12 -9
  43. package/lib/plugin/autoLogin.js +45 -10
  44. package/lib/plugin/heal.js +47 -17
  45. package/lib/plugin/retryFailedStep.js +10 -1
  46. package/lib/plugin/retryTo.js +2 -4
  47. package/lib/plugin/selenoid.js +6 -1
  48. package/lib/plugin/standardActingHelpers.js +0 -2
  49. package/lib/plugin/stepByStepReport.js +2 -2
  50. package/lib/plugin/tryTo.js +5 -7
  51. package/lib/plugin/wdio.js +0 -1
  52. package/lib/recorder.js +20 -9
  53. package/lib/session.js +1 -1
  54. package/lib/step.js +30 -11
  55. package/lib/ui.js +1 -0
  56. package/lib/utils.js +18 -1
  57. package/lib/workers.js +28 -3
  58. package/package.json +108 -98
  59. package/translations/de-DE.js +5 -0
  60. package/translations/fr-FR.js +14 -1
  61. package/translations/it-IT.js +1 -0
  62. package/translations/ja-JP.js +5 -0
  63. package/translations/pl-PL.js +5 -0
  64. package/translations/pt-BR.js +1 -0
  65. package/translations/ru-RU.js +1 -0
  66. package/translations/zh-CN.js +5 -0
  67. package/translations/zh-TW.js +5 -0
  68. package/typings/index.d.ts +8 -6
  69. package/typings/promiseBasedTypes.d.ts +784 -822
  70. package/typings/types.d.ts +1214 -727
  71. package/CHANGELOG.md +0 -2492
  72. package/docs/advanced.md +0 -351
  73. package/docs/ai.md +0 -246
  74. package/docs/api.md +0 -323
  75. package/docs/basics.md +0 -980
  76. package/docs/bdd.md +0 -535
  77. package/docs/best.md +0 -237
  78. package/docs/books.md +0 -37
  79. package/docs/bootstrap.md +0 -135
  80. package/docs/build/ApiDataFactory.js +0 -409
  81. package/docs/build/Appium.js +0 -1978
  82. package/docs/build/FileSystem.js +0 -228
  83. package/docs/build/GraphQL.js +0 -204
  84. package/docs/build/GraphQLDataFactory.js +0 -309
  85. package/docs/build/JSONResponse.js +0 -338
  86. package/docs/build/Mochawesome.js +0 -71
  87. package/docs/build/Nightmare.js +0 -2147
  88. package/docs/build/OpenAI.js +0 -122
  89. package/docs/build/Playwright.js +0 -4134
  90. package/docs/build/Polly.js +0 -42
  91. package/docs/build/Protractor.js +0 -2701
  92. package/docs/build/Puppeteer.js +0 -3743
  93. package/docs/build/REST.js +0 -344
  94. package/docs/build/SeleniumWebdriver.js +0 -76
  95. package/docs/build/TestCafe.js +0 -2059
  96. package/docs/build/WebDriver.js +0 -4042
  97. package/docs/changelog.md +0 -2501
  98. package/docs/commands.md +0 -254
  99. package/docs/community-helpers.md +0 -58
  100. package/docs/configuration.md +0 -157
  101. package/docs/continuous-integration.md +0 -22
  102. package/docs/custom-helpers.md +0 -306
  103. package/docs/data.md +0 -375
  104. package/docs/detox.md +0 -235
  105. package/docs/docker.md +0 -137
  106. package/docs/email.md +0 -183
  107. package/docs/examples.md +0 -149
  108. package/docs/helpers/ApiDataFactory.md +0 -266
  109. package/docs/helpers/Appium.md +0 -1317
  110. package/docs/helpers/Detox.md +0 -586
  111. package/docs/helpers/FileSystem.md +0 -152
  112. package/docs/helpers/GraphQL.md +0 -130
  113. package/docs/helpers/GraphQLDataFactory.md +0 -226
  114. package/docs/helpers/JSONResponse.md +0 -254
  115. package/docs/helpers/Mochawesome.md +0 -8
  116. package/docs/helpers/MockRequest.md +0 -377
  117. package/docs/helpers/Nightmare.md +0 -1258
  118. package/docs/helpers/OpenAI.md +0 -70
  119. package/docs/helpers/Playwright.md +0 -2250
  120. package/docs/helpers/Polly.md +0 -44
  121. package/docs/helpers/Puppeteer-firefox.md +0 -86
  122. package/docs/helpers/Puppeteer.md +0 -2147
  123. package/docs/helpers/REST.md +0 -218
  124. package/docs/helpers/TestCafe.md +0 -1224
  125. package/docs/helpers/WebDriver.md +0 -2325
  126. package/docs/hooks.md +0 -340
  127. package/docs/index.md +0 -111
  128. package/docs/installation.md +0 -75
  129. package/docs/internal-api.md +0 -265
  130. package/docs/locators.md +0 -331
  131. package/docs/mobile-react-native-locators.md +0 -67
  132. package/docs/mobile.md +0 -344
  133. package/docs/nightmare.md +0 -223
  134. package/docs/pageobjects.md +0 -291
  135. package/docs/parallel.md +0 -288
  136. package/docs/playwright.md +0 -609
  137. package/docs/plugins.md +0 -1225
  138. package/docs/puppeteer.md +0 -316
  139. package/docs/quickstart.md +0 -163
  140. package/docs/react.md +0 -69
  141. package/docs/reports.md +0 -392
  142. package/docs/secrets.md +0 -36
  143. package/docs/shadow.md +0 -68
  144. package/docs/shared/keys.mustache +0 -31
  145. package/docs/shared/react.mustache +0 -1
  146. package/docs/testcafe.md +0 -174
  147. package/docs/translation.md +0 -247
  148. package/docs/tutorial.md +0 -271
  149. package/docs/typescript.md +0 -180
  150. package/docs/ui.md +0 -59
  151. package/docs/videos.md +0 -28
  152. package/docs/visual.md +0 -202
  153. package/docs/vue.md +0 -121
  154. package/docs/webapi/amOnPage.mustache +0 -11
  155. package/docs/webapi/appendField.mustache +0 -11
  156. package/docs/webapi/attachFile.mustache +0 -12
  157. package/docs/webapi/checkOption.mustache +0 -13
  158. package/docs/webapi/clearCookie.mustache +0 -10
  159. package/docs/webapi/clearField.mustache +0 -9
  160. package/docs/webapi/click.mustache +0 -25
  161. package/docs/webapi/clickLink.mustache +0 -8
  162. package/docs/webapi/closeCurrentTab.mustache +0 -7
  163. package/docs/webapi/closeOtherTabs.mustache +0 -8
  164. package/docs/webapi/dontSee.mustache +0 -11
  165. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  166. package/docs/webapi/dontSeeCookie.mustache +0 -8
  167. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  168. package/docs/webapi/dontSeeElement.mustache +0 -8
  169. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  170. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  171. package/docs/webapi/dontSeeInField.mustache +0 -11
  172. package/docs/webapi/dontSeeInSource.mustache +0 -8
  173. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  174. package/docs/webapi/doubleClick.mustache +0 -13
  175. package/docs/webapi/downloadFile.mustache +0 -12
  176. package/docs/webapi/dragAndDrop.mustache +0 -9
  177. package/docs/webapi/dragSlider.mustache +0 -11
  178. package/docs/webapi/executeAsyncScript.mustache +0 -24
  179. package/docs/webapi/executeScript.mustache +0 -26
  180. package/docs/webapi/fillField.mustache +0 -16
  181. package/docs/webapi/forceClick.mustache +0 -28
  182. package/docs/webapi/forceRightClick.mustache +0 -18
  183. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  184. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  185. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  186. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  187. package/docs/webapi/grabCookie.mustache +0 -11
  188. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  189. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  190. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  191. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  192. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  193. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  194. package/docs/webapi/grabGeoLocation.mustache +0 -8
  195. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  196. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  197. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  198. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  199. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  200. package/docs/webapi/grabPopupText.mustache +0 -5
  201. package/docs/webapi/grabSource.mustache +0 -8
  202. package/docs/webapi/grabTextFrom.mustache +0 -10
  203. package/docs/webapi/grabTextFromAll.mustache +0 -9
  204. package/docs/webapi/grabTitle.mustache +0 -8
  205. package/docs/webapi/grabValueFrom.mustache +0 -9
  206. package/docs/webapi/grabValueFromAll.mustache +0 -8
  207. package/docs/webapi/moveCursorTo.mustache +0 -12
  208. package/docs/webapi/openNewTab.mustache +0 -7
  209. package/docs/webapi/pressKey.mustache +0 -12
  210. package/docs/webapi/pressKeyDown.mustache +0 -12
  211. package/docs/webapi/pressKeyUp.mustache +0 -12
  212. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  213. package/docs/webapi/refreshPage.mustache +0 -6
  214. package/docs/webapi/resizeWindow.mustache +0 -6
  215. package/docs/webapi/rightClick.mustache +0 -14
  216. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  217. package/docs/webapi/saveScreenshot.mustache +0 -12
  218. package/docs/webapi/say.mustache +0 -10
  219. package/docs/webapi/scrollIntoView.mustache +0 -11
  220. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  221. package/docs/webapi/scrollPageToTop.mustache +0 -6
  222. package/docs/webapi/scrollTo.mustache +0 -12
  223. package/docs/webapi/see.mustache +0 -11
  224. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  225. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  226. package/docs/webapi/seeCookie.mustache +0 -8
  227. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  228. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  229. package/docs/webapi/seeElement.mustache +0 -8
  230. package/docs/webapi/seeElementInDOM.mustache +0 -8
  231. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  232. package/docs/webapi/seeInField.mustache +0 -12
  233. package/docs/webapi/seeInPopup.mustache +0 -8
  234. package/docs/webapi/seeInSource.mustache +0 -7
  235. package/docs/webapi/seeInTitle.mustache +0 -8
  236. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  237. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  238. package/docs/webapi/seeTextEquals.mustache +0 -9
  239. package/docs/webapi/seeTitleEquals.mustache +0 -8
  240. package/docs/webapi/selectOption.mustache +0 -21
  241. package/docs/webapi/setCookie.mustache +0 -16
  242. package/docs/webapi/setGeoLocation.mustache +0 -12
  243. package/docs/webapi/switchTo.mustache +0 -9
  244. package/docs/webapi/switchToNextTab.mustache +0 -10
  245. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  246. package/docs/webapi/type.mustache +0 -21
  247. package/docs/webapi/uncheckOption.mustache +0 -13
  248. package/docs/webapi/wait.mustache +0 -8
  249. package/docs/webapi/waitForClickable.mustache +0 -11
  250. package/docs/webapi/waitForDetached.mustache +0 -10
  251. package/docs/webapi/waitForElement.mustache +0 -11
  252. package/docs/webapi/waitForEnabled.mustache +0 -6
  253. package/docs/webapi/waitForFunction.mustache +0 -17
  254. package/docs/webapi/waitForInvisible.mustache +0 -10
  255. package/docs/webapi/waitForText.mustache +0 -13
  256. package/docs/webapi/waitForValue.mustache +0 -10
  257. package/docs/webapi/waitForVisible.mustache +0 -10
  258. package/docs/webapi/waitInUrl.mustache +0 -9
  259. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  260. package/docs/webapi/waitToHide.mustache +0 -10
  261. package/docs/webapi/waitUrlEquals.mustache +0 -10
  262. package/docs/webdriver.md +0 -657
  263. package/docs/wiki/Books-&-Posts.md +0 -27
  264. package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
  265. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
  266. package/docs/wiki/Examples.md +0 -139
  267. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
  268. package/docs/wiki/Home.md +0 -16
  269. package/docs/wiki/Release-Process.md +0 -24
  270. package/docs/wiki/Roadmap.md +0 -23
  271. package/docs/wiki/Tests.md +0 -1393
  272. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
  273. package/docs/wiki/Videos.md +0 -19
@@ -0,0 +1,17 @@
1
+ module.exports.focusElement = (element, context) => {
2
+ const clientSideFn = el => {
3
+ el.focus();
4
+ };
5
+
6
+ try {
7
+ // Puppeteer
8
+ context.evaluate(clientSideFn, element);
9
+ } catch (e) {
10
+ // WebDriver
11
+ try {
12
+ context.execute(clientSideFn, element);
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ }
17
+ };
@@ -7,8 +7,8 @@ module.exports.highlightElement = (element, context) => {
7
7
  };
8
8
 
9
9
  try {
10
- // Playwright, Puppeteer
11
- context.evaluate(clientSideHighlightFn, element);
10
+ // Puppeteer
11
+ context.evaluate(clientSideHighlightFn, element).catch(err => console.error(err));
12
12
  } catch (e) {
13
13
  // WebDriver
14
14
  try {
package/lib/html.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { parse, serialize } = require('parse5');
2
- const { minify } = require('html-minifier');
2
+ const { minify } = require('html-minifier-terser');
3
3
 
4
- function minifyHtml(html) {
4
+ async function minifyHtml(html) {
5
5
  return minify(html, {
6
6
  collapseWhitespace: true,
7
7
  removeComments: true,
@@ -11,7 +11,7 @@ function minifyHtml(html) {
11
11
  removeStyleLinkTypeAttributes: true,
12
12
  collapseBooleanAttributes: true,
13
13
  useShortDoctype: true,
14
- }).toString();
14
+ });
15
15
  }
16
16
 
17
17
  const defaultHtmlOpts = {
@@ -30,7 +30,7 @@ const parameterTypeRegistry = new ParameterTypeRegistry();
30
30
  const matchStep = (step) => {
31
31
  for (const stepName in steps) {
32
32
  if (stepName.indexOf('/') === 0) {
33
- const regExpArr = stepName.match(new RegExp('^/(.*?)/([gimy]*)$')) || [];
33
+ const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || [];
34
34
  const res = step.match(new RegExp(regExpArr[1], regExpArr[2]));
35
35
  if (res) {
36
36
  const fn = steps[stepName];
@@ -1,6 +1,7 @@
1
1
  const Gherkin = require('@cucumber/gherkin');
2
2
  const Messages = require('@cucumber/messages');
3
3
  const { Context, Suite, Test } = require('mocha');
4
+ const debug = require('debug')('codeceptjs:bdd');
4
5
 
5
6
  const { matchStep } = require('./bdd');
6
7
  const event = require('../event');
@@ -17,6 +18,11 @@ parser.stopAtFirstError = false;
17
18
 
18
19
  module.exports = (text, file) => {
19
20
  const ast = parser.parse(text);
21
+ let currentLanguage;
22
+
23
+ if (ast.feature) {
24
+ currentLanguage = getTranslation(ast.feature.language);
25
+ }
20
26
 
21
27
  if (!ast.feature) {
22
28
  throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);
@@ -39,7 +45,9 @@ module.exports = (text, file) => {
39
45
  for (const step of steps) {
40
46
  const metaStep = new Step.MetaStep(null, step.text);
41
47
  metaStep.actor = step.keyword.trim();
48
+ let helperStep;
42
49
  const setMetaStep = (step) => {
50
+ helperStep = step;
43
51
  if (step.metaStep) {
44
52
  if (step.metaStep === metaStep) {
45
53
  return;
@@ -67,11 +75,15 @@ module.exports = (text, file) => {
67
75
  step.startTime = Date.now();
68
76
  step.match = fn.line;
69
77
  event.emit(event.bddStep.before, step);
78
+ event.emit(event.bddStep.started, metaStep);
70
79
  event.dispatcher.prependListener(event.step.before, setMetaStep);
71
80
  try {
81
+ debug(`Step '${step.text}' started...`);
72
82
  await fn(...fn.params);
83
+ debug('Step passed');
73
84
  step.status = 'passed';
74
85
  } catch (err) {
86
+ debug(`Step failed: ${err?.message}`);
75
87
  step.status = 'failed';
76
88
  step.err = err;
77
89
  throw err;
@@ -79,6 +91,7 @@ module.exports = (text, file) => {
79
91
  step.endTime = Date.now();
80
92
  event.dispatcher.removeListener(event.step.before, setMetaStep);
81
93
  }
94
+ event.emit(event.bddStep.finished, metaStep);
82
95
  event.emit(event.bddStep.after, step);
83
96
  }
84
97
  };
@@ -88,7 +101,7 @@ module.exports = (text, file) => {
88
101
  suite.beforeEach('Before', scenario.injected(async () => runSteps(child.background.steps), suite, 'before'));
89
102
  continue;
90
103
  }
91
- if (child.scenario && child.scenario.keyword === 'Scenario Outline') {
104
+ if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) {
92
105
  for (const examples of child.scenario.examples) {
93
106
  const fields = examples.tableHeader.cells.map(c => c.value);
94
107
  for (const example of examples.tableBody) {
@@ -106,7 +119,14 @@ module.exports = (text, file) => {
106
119
  });
107
120
  }
108
121
  const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
109
- const title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
122
+ let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
123
+
124
+ for (const [key, value] of Object.entries(current)) {
125
+ if (title.includes(`<${key}>`)) {
126
+ title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value);
127
+ }
128
+ }
129
+
110
130
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
111
131
  test.tags = suite.tags.concat(tags);
112
132
  test.file = file;
@@ -133,7 +153,7 @@ function transformTable(table) {
133
153
  let str = '';
134
154
  for (const id in table.rows) {
135
155
  const cells = table.rows[id].cells;
136
- str += cells.map(c => c.value).map(c => c.slice(0, 15).padEnd(15)).join(' | ');
156
+ str += cells.map(c => c.value).map(c => c.padEnd(15)).join(' | ');
137
157
  str += '\n';
138
158
  }
139
159
  return str;
@@ -154,3 +174,17 @@ function addExampleInTable(exampleSteps, placeholders) {
154
174
  }
155
175
  return steps;
156
176
  }
177
+
178
+ function getTranslation(language) {
179
+ const translations = Object.keys(require('../../translations'));
180
+
181
+ for (const availableTranslation of translations) {
182
+ if (!language) {
183
+ break;
184
+ }
185
+
186
+ if (availableTranslation.includes(language)) {
187
+ return require('../../translations')[availableTranslation];
188
+ }
189
+ }
190
+ }
@@ -35,6 +35,7 @@ class ScenarioConfig {
35
35
  * @returns {this}
36
36
  */
37
37
  retry(retries) {
38
+ if (process.env.SCENARIO_ONLY) retries = -retries;
38
39
  this.test.retries(retries);
39
40
  return this;
40
41
  }
package/lib/locator.js CHANGED
@@ -1,4 +1,4 @@
1
- const cssToXPath = require('css-to-xpath');
1
+ const cssToXPath = require('csstoxpath');
2
2
  const { sprintf } = require('sprintf-js');
3
3
 
4
4
  const { xpathLocator } = require('./utils');
@@ -158,11 +158,12 @@ class Locator {
158
158
  }
159
159
 
160
160
  /**
161
+ * @param {string} [pseudoSelector] CSS to XPath extension pseudo: https://www.npmjs.com/package/csstoxpath?activeTab=explore#extension-pseudos
161
162
  * @returns {string}
162
163
  */
163
- toXPath() {
164
+ toXPath(pseudoSelector = '') {
164
165
  if (this.isXPath()) return this.value;
165
- if (this.isCSS()) return cssToXPath(this.value);
166
+ if (this.isCSS()) return cssToXPath(`${this.value}${pseudoSelector}`);
166
167
 
167
168
  throw new Error('Can\'t be converted to XPath');
168
169
  }
@@ -243,12 +244,24 @@ class Locator {
243
244
  }
244
245
 
245
246
  /**
247
+ * Find an element containing a text
246
248
  * @param {string} text
247
249
  * @returns {Locator}
248
250
  */
249
251
  withText(text) {
250
252
  text = xpathLocator.literal(text);
251
- const xpath = sprintf('%s[%s]', this.toXPath(), `contains(., ${text})`);
253
+ const xpath = this.toXPath(`:text-contains-case(${text})`);
254
+ return new Locator({ xpath });
255
+ }
256
+
257
+ /**
258
+ * Find an element with exact text
259
+ * @param {string} text
260
+ * @returns {Locator}
261
+ */
262
+ withTextEquals(text) {
263
+ text = xpathLocator.literal(text);
264
+ const xpath = this.toXPath(`:text-case(${text})`);
252
265
  return new Locator({ xpath });
253
266
  }
254
267
 
@@ -97,7 +97,8 @@ class MochaFactory {
97
97
  const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter');
98
98
  if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
99
99
  Object.defineProperty(
100
- reporterOptions, 'codeceptjs/lib/cli',
100
+ reporterOptions,
101
+ 'codeceptjs/lib/cli',
101
102
  attributes,
102
103
  );
103
104
  delete reporterOptions['codeceptjs-cli-reporter'];
package/lib/output.js CHANGED
@@ -106,7 +106,7 @@ module.exports = {
106
106
  if (!step) return;
107
107
  // Avoid to print non-gherkin steps, when gherkin is running for --steps mode
108
108
  if (outputLevel === 1) {
109
- if (step.hasBDDAncestor()) {
109
+ if (typeof step === 'object' && step.hasBDDAncestor()) {
110
110
  return;
111
111
  }
112
112
  }
package/lib/pause.js CHANGED
@@ -18,10 +18,10 @@ let nextStep;
18
18
  let finish;
19
19
  let next;
20
20
  let registeredVariables = {};
21
- const aiAssistant = new AiAssistant();
22
-
21
+ let aiAssistant;
23
22
  /**
24
23
  * Pauses test execution and starts interactive shell
24
+ * @param {Object<string, *>} [passedObject]
25
25
  */
26
26
  const pause = function (passedObject = {}) {
27
27
  if (store.dryRun) return;
@@ -44,6 +44,8 @@ function pauseSession(passedObject = {}) {
44
44
  let vars = Object.keys(registeredVariables).join(', ');
45
45
  if (vars) vars = `(vars: ${vars})`;
46
46
 
47
+ aiAssistant = AiAssistant.getInstance();
48
+
47
49
  output.print(colors.yellow(' Interactive shell started'));
48
50
  output.print(colors.yellow(' Use JavaScript syntax to try steps in action'));
49
51
  output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`));
@@ -67,6 +69,7 @@ function pauseSession(passedObject = {}) {
67
69
  });
68
70
  return new Promise(((resolve) => {
69
71
  finish = resolve;
72
+ // eslint-disable-next-line
70
73
  return askForStep();
71
74
  }));
72
75
  }
@@ -76,7 +79,6 @@ async function parseInput(cmd) {
76
79
  rl.pause();
77
80
  next = false;
78
81
  recorder.session.start('pause');
79
- store.debugMode = false;
80
82
  if (cmd === '') next = true;
81
83
  if (!cmd || cmd === 'resume' || cmd === 'exit') {
82
84
  finish();
@@ -96,13 +98,14 @@ async function parseInput(cmd) {
96
98
  return cmd;
97
99
  };
98
100
 
99
- store.debugMode = true;
100
101
  let isCustomCommand = false;
101
102
  let lastError = null;
102
103
  let isAiCommand = false;
103
104
  let $res;
104
105
  try {
106
+ // eslint-disable-next-line
105
107
  const locate = global.locate; // enable locate in this context
108
+ // eslint-disable-next-line
106
109
  const I = container.support('I');
107
110
  if (cmd.trim().startsWith('=>')) {
108
111
  isCustomCommand = true;
@@ -114,13 +117,13 @@ async function parseInput(cmd) {
114
117
  isAiCommand = true;
115
118
  executeCommand = executeCommand.then(async () => {
116
119
  try {
117
- const html = await res;
118
- aiAssistant.setHtmlContext(html);
120
+ const html = await res;
121
+ await aiAssistant.setHtmlContext(html);
119
122
  } catch (err) {
120
123
  output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
121
124
  return;
122
125
  } finally {
123
- output.level(currentOutputLevel);
126
+ output.level(currentOutputLevel);
124
127
  }
125
128
  // aiAssistant.mockResponse("```js\nI.click('Sign in');\n```");
126
129
  const spinner = ora("Processing OpenAI request...").start();
@@ -148,12 +151,12 @@ async function parseInput(cmd) {
148
151
  })
149
152
 
150
153
  const val = await executeCommand;
151
-
154
+
152
155
  if (isCustomCommand) {
153
156
  if (val !== undefined) console.log('Result', '$res=', val); // eslint-disable-line
154
157
  $res = val;
155
158
  }
156
-
159
+
157
160
  if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
158
161
  output.print(output.styles.success(' OK '), cmd);
159
162
  }
@@ -9,6 +9,7 @@ const isAsyncFunction = require('../utils').isAsyncFunction;
9
9
 
10
10
  const defaultUser = {
11
11
  fetch: I => I.grabCookie(),
12
+ check: () => {},
12
13
  restore: (I, cookies) => {
13
14
  I.amOnPage('/'); // open a page
14
15
  I.setCookie(cookies);
@@ -37,12 +38,14 @@ const defaultConfig = {
37
38
  * ```js
38
39
  * // inside a test file
39
40
  * // use login to inject auto-login function
41
+ * Feature('Login');
42
+ *
40
43
  * Before(({ login }) => {
41
44
  * login('user'); // login using user session
42
45
  * });
43
46
  *
44
- * // Alternatively log in for one scenario
45
- * Scenario('log me in', ( {I, login} ) => {
47
+ * // Alternatively log in for one scenario.
48
+ * Scenario('log me in', ( { I, login } ) => {
46
49
  * login('admin');
47
50
  * I.see('I am logged in');
48
51
  * });
@@ -61,7 +64,7 @@ const defaultConfig = {
61
64
  * #### How It Works
62
65
  *
63
66
  * 1. `restore` method is executed. It should open a page and set credentials.
64
- * 2. `check` method is executed. It should reload a page (so cookies are applied) and check that this page belongs to logged in user.
67
+ * 2. `check` method is executed. It should reload a page (so cookies are applied) and check that this page belongs to logged-in user. When you pass the second args `session`, you could perform the validation using passed session.
65
68
  * 3. If `restore` and `check` were not successful, `login` is executed
66
69
  * 4. `login` should fill in login form
67
70
  * 5. After successful login, `fetch` is executed to save cookies into memory or file.
@@ -212,6 +215,38 @@ const defaultConfig = {
212
215
  * })
213
216
  * ```
214
217
  *
218
+ * #### Tips: Using session to validate user
219
+ *
220
+ * Instead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch`
221
+ *
222
+ * ```js
223
+ * autoLogin: {
224
+ * enabled: true,
225
+ * saveToFile: true,
226
+ * inject: 'login',
227
+ * users: {
228
+ * admin: {
229
+ * login: async (I) => { // If you use async function in the autoLogin plugin
230
+ * const phrase = await I.grabTextFrom('#phrase')
231
+ * I.fillField('username', 'admin'),
232
+ * I.fillField('password', 'password')
233
+ * I.fillField('phrase', phrase)
234
+ * },
235
+ * check: (I, session) => {
236
+ * // Throwing an error in `check` will make CodeceptJS perform the login step for the user
237
+ * if (session.profile.email !== the.email.you.expect@some-mail.com) {
238
+ * throw new Error ('Wrong user signed in');
239
+ * }
240
+ * },
241
+ * }
242
+ * }
243
+ * }
244
+ * ```
245
+ *
246
+ * ```js
247
+ * Scenario('login', async ( {I, login} ) => {
248
+ * await login('admin') // you should use `await`
249
+ * })
215
250
  *
216
251
  *
217
252
  */
@@ -251,27 +286,28 @@ module.exports = function (config) {
251
286
  } else {
252
287
  userSession.login(I);
253
288
  }
254
- store.debugMode = true;
289
+
255
290
  const cookies = await userSession.fetch(I);
291
+ if (!cookies) {
292
+ debug('Cannot save user session with empty cookies from auto login\'s fetch method');
293
+ return;
294
+ }
256
295
  if (config.saveToFile) {
257
296
  debug(`Saved user session into file for ${name}`);
258
297
  fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies));
259
298
  }
260
299
  store[`${name}_session`] = cookies;
261
- store.debugMode = false;
262
300
  };
263
301
 
264
302
  if (!cookies) return loginAndSave();
265
303
 
266
- store.debugMode = true;
267
-
268
304
  recorder.session.start('check login');
269
305
  if (shouldAwait) {
270
306
  await userSession.restore(I, cookies);
271
- await userSession.check(I);
307
+ await userSession.check(I, cookies);
272
308
  } else {
273
309
  userSession.restore(I, cookies);
274
- userSession.check(I);
310
+ userSession.check(I, cookies);
275
311
  }
276
312
  recorder.session.catch((err) => {
277
313
  debug(`Failed auto login for ${name} due to ${err}`);
@@ -287,7 +323,6 @@ module.exports = function (config) {
287
323
  });
288
324
  });
289
325
  recorder.add(() => {
290
- store.debugMode = false;
291
326
  recorder.session.restore('check login');
292
327
  });
293
328
 
@@ -8,6 +8,7 @@ const output = require('../output');
8
8
  const supportedHelpers = require('./standardActingHelpers');
9
9
 
10
10
  const defaultConfig = {
11
+ healTries: 1,
11
12
  healLimit: 2,
12
13
  healSteps: [
13
14
  'click',
@@ -26,7 +27,7 @@ const defaultConfig = {
26
27
  *
27
28
  * This plugin is experimental and requires OpenAI API key.
28
29
  *
29
- * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside condig.
30
+ * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
30
31
  *
31
32
  * ```js
32
33
  * plugins: {
@@ -54,11 +55,14 @@ const defaultConfig = {
54
55
  *
55
56
  */
56
57
  module.exports = function (config = {}) {
57
- const aiAssistant = new AiAssistant();
58
+ const aiAssistant = AiAssistant.getInstance();
58
59
 
59
60
  let currentTest = null;
60
61
  let currentStep = null;
61
62
  let healedSteps = 0;
63
+ let caughtError;
64
+ let healTries = 0;
65
+ let isHealing = false;
62
66
 
63
67
  const healSuggestions = [];
64
68
 
@@ -67,22 +71,37 @@ module.exports = function (config = {}) {
67
71
  event.dispatcher.on(event.test.before, (test) => {
68
72
  currentTest = test;
69
73
  healedSteps = 0;
74
+ caughtError = null;
70
75
  });
71
76
 
72
77
  event.dispatcher.on(event.step.started, step => currentStep = step);
73
78
 
74
- event.dispatcher.on(event.step.before, () => {
79
+ event.dispatcher.on(event.step.after, (step) => {
80
+ if (isHealing) return;
75
81
  const store = require('../store');
76
82
  if (store.debugMode) return;
77
-
78
83
  recorder.catchWithoutStop(async (err) => {
79
- if (!aiAssistant.isEnabled) throw err;
84
+ isHealing = true;
85
+ if (caughtError === err) throw err; // avoid double handling
86
+ caughtError = err;
87
+ if (!aiAssistant.isEnabled) {
88
+ output.print(colors.yellow('Heal plugin can\'t operate, AI assistant is disabled. Please set OPENAI_API_KEY env variable to enable it.'));
89
+ throw err;
90
+ }
80
91
  if (!currentStep) throw err;
81
92
  if (!config.healSteps.includes(currentStep.name)) throw err;
82
93
  const test = currentTest;
83
94
 
95
+ if (healTries >= config.healTries) {
96
+ output.print(colors.bold.red(`Healing failed for ${config.healTries} time(s)`));
97
+ output.print('AI couldn\'t identify the correct solution');
98
+ output.print('Probably the entire flow has changed and the test should be updated');
99
+
100
+ throw err;
101
+ }
102
+
84
103
  if (healedSteps >= config.healLimit) {
85
- output.print(colors.bold.red(`Can't heal more than ${config.healLimit} steps in a test`));
104
+ output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
86
105
  output.print('Entire flow can be broken, please check it manually');
87
106
  output.print('or increase healing limit in heal plugin config');
88
107
 
@@ -111,9 +130,17 @@ module.exports = function (config = {}) {
111
130
 
112
131
  if (!html) throw err;
113
132
 
114
- aiAssistant.setHtmlContext(html);
133
+ healTries++;
134
+ await aiAssistant.setHtmlContext(html);
115
135
  await tryToHeal(step, err);
116
- recorder.session.restore();
136
+
137
+ recorder.add('close healing session', () => {
138
+ recorder.session.restore('heal');
139
+ recorder.ignoreErr(err);
140
+ });
141
+ await recorder.promise();
142
+
143
+ isHealing = false;
117
144
  });
118
145
  });
119
146
 
@@ -126,7 +153,7 @@ module.exports = function (config = {}) {
126
153
  print('===================');
127
154
  print(colors.bold.green('Self-Healing Report:'));
128
155
 
129
- print(`${colors.bold(healSuggestions.length)} steps were healed by AI`);
156
+ print(`${colors.bold(healSuggestions.length)} step(s) were healed by AI`);
130
157
 
131
158
  let i = 1;
132
159
  print('');
@@ -145,19 +172,19 @@ module.exports = function (config = {}) {
145
172
  });
146
173
 
147
174
  async function tryToHeal(failedStep, err) {
148
- output.debug(`Running OpenAPI to heal ${failedStep.toCode()} step`);
175
+ output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
149
176
 
150
- const I = Container.support('I');
177
+ const codeSnippets = await aiAssistant.healFailedStep(failedStep, err, currentTest);
151
178
 
152
- const codeSnippets = await aiAssistant.healFailedStep(
153
- failedStep, err, currentTest,
154
- );
155
-
156
- output.debug(`Received ${codeSnippets.length} proposals from OpenAI`);
179
+ output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
180
+ const I = Container.support('I'); // eslint-disable-line
157
181
 
158
182
  for (const codeSnippet of codeSnippets) {
159
183
  try {
160
184
  debug('Executing', codeSnippet);
185
+ recorder.catch((e) => {
186
+ console.log(e);
187
+ });
161
188
  await eval(codeSnippet); // eslint-disable-line
162
189
 
163
190
  healSuggestions.push({
@@ -166,14 +193,17 @@ module.exports = function (config = {}) {
166
193
  snippet: codeSnippet,
167
194
  });
168
195
 
169
- output.print(colors.bold.green(' Code healed successfully'));
196
+ recorder.add('healed', () => output.print(colors.bold.green(' Code healed successfully')));
170
197
  healedSteps++;
171
198
  return;
172
199
  } catch (err) {
173
200
  debug('Failed to execute code', err);
201
+ recorder.ignoreErr(err); // healing ded not help
202
+ // recorder.catch(() => output.print(colors.bold.red(' Failed healing code')));
174
203
  }
175
204
  }
176
205
 
177
206
  output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
178
207
  }
208
+ return recorder.promise();
179
209
  };
@@ -1,5 +1,7 @@
1
1
  const event = require('../event');
2
2
  const recorder = require('../recorder');
3
+ const container = require('../container');
4
+ const { log } = require('../output');
3
5
 
4
6
  const defaultConfig = {
5
7
  retries: 3,
@@ -42,7 +44,7 @@ const defaultConfig = {
42
44
  * * `factor` - The exponential factor to use. Default is 1.5.
43
45
  * * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
44
46
  * * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
45
- * * `randomize` - Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.
47
+ * * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
46
48
  * * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
47
49
  * * `amOnPage`
48
50
  * * `wait*`
@@ -98,6 +100,11 @@ module.exports = (config) => {
98
100
  config.when = when;
99
101
 
100
102
  event.dispatcher.on(event.step.started, (step) => {
103
+ if (process.env.TRY_TO === 'true') {
104
+ log('Info: RetryFailedStep plugin is disabled inside tryTo block');
105
+ return;
106
+ }
107
+
101
108
  // if a step is ignored - return
102
109
  for (const ignored of config.ignoredSteps) {
103
110
  if (step.name === ignored) return;
@@ -114,6 +121,8 @@ module.exports = (config) => {
114
121
 
115
122
  event.dispatcher.on(event.test.before, (test) => {
116
123
  if (test && test.disableRetryFailedStep) return; // disable retry when a test is not active
124
+ // this env var is used to set the retries inside _before() block of helpers
125
+ process.env.FAILED_STEP_RETRIES = config.retries;
117
126
  recorder.retry(config);
118
127
  });
119
128
  };
@@ -83,16 +83,15 @@ module.exports = function (config) {
83
83
  return retryTo;
84
84
 
85
85
  function retryTo(callback, maxTries, pollInterval = undefined) {
86
- const mode = store.debugMode;
87
86
  let tries = 1;
88
87
  if (!pollInterval) pollInterval = config.pollInterval;
89
88
 
90
89
  let err = null;
91
90
 
92
91
  return new Promise((done) => {
93
- const tryBlock = () => {
92
+ const tryBlock = async () => {
94
93
  recorder.session.start(`retryTo ${tries}`);
95
- callback(tries);
94
+ await callback(tries);
96
95
  recorder.add(() => {
97
96
  recorder.session.restore(`retryTo ${tries}`);
98
97
  done(null);
@@ -113,7 +112,6 @@ module.exports = function (config) {
113
112
  };
114
113
 
115
114
  recorder.add('retryTo', async () => {
116
- store.debugMode = true;
117
115
  tryBlock();
118
116
  });
119
117
  }).then(() => {
@@ -58,7 +58,12 @@ let seleniumUrl = 'http://localhost:$port$';
58
58
  const supportedHelpers = ['WebDriver'];
59
59
  const SELENOID_START_TIMEOUT = 2000;
60
60
  const SELENOID_STOP_TIMEOUT = 10000;
61
- const wait = time => new Promise((res) => setTimeout(() => res(), time));
61
+ const wait = time => new Promise((res) => {
62
+ setTimeout(() => {
63
+ // @ts-ignore
64
+ res();
65
+ }, time);
66
+ });
62
67
 
63
68
  /**
64
69
  * [Selenoid](https://aerokube.com/selenoid/) plugin automatically starts browsers and video recording.
@@ -4,8 +4,6 @@ const standardActingHelpers = [
4
4
  'Puppeteer',
5
5
  'Appium',
6
6
  'TestCafe',
7
- 'Protractor',
8
- 'Nightmare',
9
7
  ];
10
8
 
11
9
  module.exports = standardActingHelpers;