codeceptjs 3.4.1 → 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 (281) hide show
  1. package/README.md +31 -30
  2. package/bin/codecept.js +1 -1
  3. package/lib/actor.js +6 -3
  4. package/lib/ai.js +180 -0
  5. package/lib/cli.js +13 -3
  6. package/lib/codecept.js +8 -0
  7. package/lib/colorUtils.js +10 -0
  8. package/lib/command/definitions.js +2 -7
  9. package/lib/command/dryRun.js +11 -2
  10. package/lib/command/generate.js +46 -3
  11. package/lib/command/info.js +24 -0
  12. package/lib/command/init.js +64 -6
  13. package/lib/command/interactive.js +15 -1
  14. package/lib/command/run-multiple/collection.js +17 -5
  15. package/lib/command/run-multiple.js +4 -2
  16. package/lib/command/run-workers.js +68 -5
  17. package/lib/command/run.js +7 -0
  18. package/lib/command/workers/runTests.js +39 -0
  19. package/lib/container.js +13 -3
  20. package/lib/data/context.js +14 -6
  21. package/lib/event.js +4 -0
  22. package/lib/helper/ApiDataFactory.js +2 -1
  23. package/lib/helper/Appium.js +116 -29
  24. package/lib/helper/Expect.js +422 -0
  25. package/lib/helper/FileSystem.js +1 -1
  26. package/lib/helper/GraphQL.js +25 -0
  27. package/lib/helper/JSONResponse.js +4 -4
  28. package/lib/helper/Nightmare.js +10 -5
  29. package/lib/helper/OpenAI.js +126 -0
  30. package/lib/helper/Playwright.js +1298 -229
  31. package/lib/helper/Protractor.js +12 -7
  32. package/lib/helper/Puppeteer.js +204 -64
  33. package/lib/helper/REST.js +15 -5
  34. package/lib/helper/TestCafe.js +45 -10
  35. package/lib/helper/WebDriver.js +252 -83
  36. package/lib/helper/errors/ElementNotFound.js +2 -1
  37. package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
  38. package/lib/helper/scripts/blurElement.js +17 -0
  39. package/lib/helper/scripts/focusElement.js +17 -0
  40. package/lib/helper/scripts/highlightElement.js +20 -0
  41. package/lib/html.js +258 -0
  42. package/lib/interfaces/bdd.js +1 -1
  43. package/lib/interfaces/gherkin.js +37 -3
  44. package/lib/interfaces/scenarioConfig.js +1 -0
  45. package/lib/listener/retry.js +2 -1
  46. package/lib/locator.js +17 -4
  47. package/lib/mochaFactory.js +2 -1
  48. package/lib/output.js +1 -1
  49. package/lib/pause.js +78 -19
  50. package/lib/plugin/autoLogin.js +45 -10
  51. package/lib/plugin/debugErrors.js +67 -0
  52. package/lib/plugin/fakerTransform.js +4 -6
  53. package/lib/plugin/heal.js +209 -0
  54. package/lib/plugin/retryFailedStep.js +10 -1
  55. package/lib/plugin/retryTo.js +2 -4
  56. package/lib/plugin/screenshotOnFail.js +11 -2
  57. package/lib/plugin/selenoid.js +6 -1
  58. package/lib/plugin/standardActingHelpers.js +0 -2
  59. package/lib/plugin/stepByStepReport.js +2 -2
  60. package/lib/plugin/tryTo.js +5 -7
  61. package/lib/plugin/wdio.js +0 -1
  62. package/lib/recorder.js +22 -11
  63. package/lib/secret.js +5 -4
  64. package/lib/session.js +1 -1
  65. package/lib/step.js +36 -12
  66. package/lib/ui.js +5 -3
  67. package/lib/utils.js +22 -1
  68. package/lib/workers.js +83 -10
  69. package/package.json +117 -95
  70. package/translations/de-DE.js +5 -0
  71. package/translations/fr-FR.js +14 -1
  72. package/translations/it-IT.js +1 -0
  73. package/translations/ja-JP.js +14 -9
  74. package/translations/pl-PL.js +5 -0
  75. package/translations/pt-BR.js +1 -0
  76. package/translations/ru-RU.js +1 -0
  77. package/translations/zh-CN.js +5 -0
  78. package/translations/zh-TW.js +5 -0
  79. package/typings/index.d.ts +51 -15
  80. package/typings/promiseBasedTypes.d.ts +864 -802
  81. package/typings/types.d.ts +1339 -744
  82. package/CHANGELOG.md +0 -2427
  83. package/docs/advanced.md +0 -351
  84. package/docs/api.md +0 -323
  85. package/docs/basics.md +0 -980
  86. package/docs/bdd.md +0 -535
  87. package/docs/best.md +0 -237
  88. package/docs/books.md +0 -37
  89. package/docs/bootstrap.md +0 -135
  90. package/docs/build/ApiDataFactory.js +0 -409
  91. package/docs/build/Appium.js +0 -1938
  92. package/docs/build/FileSystem.js +0 -228
  93. package/docs/build/GraphQL.js +0 -204
  94. package/docs/build/GraphQLDataFactory.js +0 -309
  95. package/docs/build/JSONResponse.js +0 -338
  96. package/docs/build/Mochawesome.js +0 -71
  97. package/docs/build/Nightmare.js +0 -2145
  98. package/docs/build/Playwright.js +0 -3986
  99. package/docs/build/Polly.js +0 -42
  100. package/docs/build/Protractor.js +0 -2699
  101. package/docs/build/Puppeteer.js +0 -3710
  102. package/docs/build/REST.js +0 -334
  103. package/docs/build/SeleniumWebdriver.js +0 -76
  104. package/docs/build/TestCafe.js +0 -2057
  105. package/docs/build/WebDriver.js +0 -4017
  106. package/docs/changelog.md +0 -2436
  107. package/docs/commands.md +0 -254
  108. package/docs/community-helpers.md +0 -58
  109. package/docs/configuration.md +0 -157
  110. package/docs/continuous-integration.md +0 -22
  111. package/docs/custom-helpers.md +0 -306
  112. package/docs/data.md +0 -375
  113. package/docs/detox.md +0 -235
  114. package/docs/docker.md +0 -137
  115. package/docs/email.md +0 -183
  116. package/docs/examples.md +0 -149
  117. package/docs/helpers/ApiDataFactory.md +0 -266
  118. package/docs/helpers/Appium.md +0 -1312
  119. package/docs/helpers/Detox.md +0 -586
  120. package/docs/helpers/FileSystem.md +0 -152
  121. package/docs/helpers/GraphQL.md +0 -130
  122. package/docs/helpers/GraphQLDataFactory.md +0 -226
  123. package/docs/helpers/JSONResponse.md +0 -254
  124. package/docs/helpers/Mochawesome.md +0 -8
  125. package/docs/helpers/MockRequest.md +0 -377
  126. package/docs/helpers/Nightmare.md +0 -1256
  127. package/docs/helpers/Playwright.md +0 -2208
  128. package/docs/helpers/Polly.md +0 -44
  129. package/docs/helpers/Puppeteer-firefox.md +0 -86
  130. package/docs/helpers/Puppeteer.md +0 -2141
  131. package/docs/helpers/REST.md +0 -217
  132. package/docs/helpers/TestCafe.md +0 -1222
  133. package/docs/helpers/WebDriver.md +0 -2319
  134. package/docs/hooks.md +0 -340
  135. package/docs/index.md +0 -111
  136. package/docs/installation.md +0 -75
  137. package/docs/internal-api.md +0 -265
  138. package/docs/locators.md +0 -331
  139. package/docs/mobile-react-native-locators.md +0 -67
  140. package/docs/mobile.md +0 -297
  141. package/docs/nightmare.md +0 -223
  142. package/docs/pageobjects.md +0 -291
  143. package/docs/parallel.md +0 -232
  144. package/docs/playwright.md +0 -609
  145. package/docs/plugins.md +0 -1171
  146. package/docs/puppeteer.md +0 -316
  147. package/docs/quickstart.md +0 -163
  148. package/docs/react.md +0 -69
  149. package/docs/reports.md +0 -392
  150. package/docs/secrets.md +0 -30
  151. package/docs/shadow.md +0 -68
  152. package/docs/shared/keys.mustache +0 -31
  153. package/docs/shared/react.mustache +0 -1
  154. package/docs/testcafe.md +0 -174
  155. package/docs/translation.md +0 -247
  156. package/docs/tutorial.md +0 -271
  157. package/docs/typescript.md +0 -180
  158. package/docs/ui.md +0 -59
  159. package/docs/videos.md +0 -28
  160. package/docs/visual.md +0 -202
  161. package/docs/vue.md +0 -121
  162. package/docs/webapi/amOnPage.mustache +0 -11
  163. package/docs/webapi/appendField.mustache +0 -9
  164. package/docs/webapi/attachFile.mustache +0 -12
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -10
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -25
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeElement.mustache +0 -8
  177. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  178. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  179. package/docs/webapi/dontSeeInField.mustache +0 -11
  180. package/docs/webapi/dontSeeInSource.mustache +0 -8
  181. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  182. package/docs/webapi/doubleClick.mustache +0 -13
  183. package/docs/webapi/downloadFile.mustache +0 -12
  184. package/docs/webapi/dragAndDrop.mustache +0 -9
  185. package/docs/webapi/dragSlider.mustache +0 -11
  186. package/docs/webapi/executeAsyncScript.mustache +0 -24
  187. package/docs/webapi/executeScript.mustache +0 -26
  188. package/docs/webapi/fillField.mustache +0 -16
  189. package/docs/webapi/forceClick.mustache +0 -28
  190. package/docs/webapi/forceRightClick.mustache +0 -18
  191. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  192. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  193. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  194. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  195. package/docs/webapi/grabCookie.mustache +0 -11
  196. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  197. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  198. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  199. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  200. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  201. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  202. package/docs/webapi/grabGeoLocation.mustache +0 -8
  203. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  204. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  205. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  206. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  207. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  208. package/docs/webapi/grabPopupText.mustache +0 -5
  209. package/docs/webapi/grabSource.mustache +0 -8
  210. package/docs/webapi/grabTextFrom.mustache +0 -10
  211. package/docs/webapi/grabTextFromAll.mustache +0 -9
  212. package/docs/webapi/grabTitle.mustache +0 -8
  213. package/docs/webapi/grabValueFrom.mustache +0 -9
  214. package/docs/webapi/grabValueFromAll.mustache +0 -8
  215. package/docs/webapi/moveCursorTo.mustache +0 -12
  216. package/docs/webapi/openNewTab.mustache +0 -7
  217. package/docs/webapi/pressKey.mustache +0 -12
  218. package/docs/webapi/pressKeyDown.mustache +0 -12
  219. package/docs/webapi/pressKeyUp.mustache +0 -12
  220. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  221. package/docs/webapi/refreshPage.mustache +0 -6
  222. package/docs/webapi/resizeWindow.mustache +0 -6
  223. package/docs/webapi/rightClick.mustache +0 -14
  224. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  225. package/docs/webapi/saveScreenshot.mustache +0 -12
  226. package/docs/webapi/say.mustache +0 -10
  227. package/docs/webapi/scrollIntoView.mustache +0 -11
  228. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  229. package/docs/webapi/scrollPageToTop.mustache +0 -6
  230. package/docs/webapi/scrollTo.mustache +0 -12
  231. package/docs/webapi/see.mustache +0 -11
  232. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  233. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  234. package/docs/webapi/seeCookie.mustache +0 -8
  235. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  236. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  237. package/docs/webapi/seeElement.mustache +0 -8
  238. package/docs/webapi/seeElementInDOM.mustache +0 -8
  239. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  240. package/docs/webapi/seeInField.mustache +0 -12
  241. package/docs/webapi/seeInPopup.mustache +0 -8
  242. package/docs/webapi/seeInSource.mustache +0 -7
  243. package/docs/webapi/seeInTitle.mustache +0 -8
  244. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  245. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  246. package/docs/webapi/seeTextEquals.mustache +0 -9
  247. package/docs/webapi/seeTitleEquals.mustache +0 -8
  248. package/docs/webapi/selectOption.mustache +0 -21
  249. package/docs/webapi/setCookie.mustache +0 -16
  250. package/docs/webapi/setGeoLocation.mustache +0 -12
  251. package/docs/webapi/switchTo.mustache +0 -9
  252. package/docs/webapi/switchToNextTab.mustache +0 -10
  253. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  254. package/docs/webapi/type.mustache +0 -18
  255. package/docs/webapi/uncheckOption.mustache +0 -13
  256. package/docs/webapi/wait.mustache +0 -8
  257. package/docs/webapi/waitForClickable.mustache +0 -11
  258. package/docs/webapi/waitForDetached.mustache +0 -10
  259. package/docs/webapi/waitForElement.mustache +0 -11
  260. package/docs/webapi/waitForEnabled.mustache +0 -6
  261. package/docs/webapi/waitForFunction.mustache +0 -17
  262. package/docs/webapi/waitForInvisible.mustache +0 -10
  263. package/docs/webapi/waitForText.mustache +0 -13
  264. package/docs/webapi/waitForValue.mustache +0 -10
  265. package/docs/webapi/waitForVisible.mustache +0 -10
  266. package/docs/webapi/waitInUrl.mustache +0 -9
  267. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  268. package/docs/webapi/waitToHide.mustache +0 -10
  269. package/docs/webapi/waitUrlEquals.mustache +0 -10
  270. package/docs/webdriver.md +0 -657
  271. package/docs/wiki/Books-&-Posts.md +0 -27
  272. package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
  273. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
  274. package/docs/wiki/Examples.md +0 -139
  275. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
  276. package/docs/wiki/Home.md +0 -16
  277. package/docs/wiki/Release-Process.md +0 -24
  278. package/docs/wiki/Roadmap.md +0 -23
  279. package/docs/wiki/Tests.md +0 -1393
  280. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
  281. package/docs/wiki/Videos.md +0 -19
package/lib/pause.js CHANGED
@@ -1,9 +1,12 @@
1
1
  const colors = require('chalk');
2
2
  const readline = require('readline');
3
+ const ora = require('ora-classic');
4
+ const debug = require('debug')('codeceptjs:pause');
3
5
 
4
6
  const container = require('./container');
5
7
  const history = require('./history');
6
8
  const store = require('./store');
9
+ const AiAssistant = require('./ai');
7
10
  const recorder = require('./recorder');
8
11
  const event = require('./event');
9
12
  const output = require('./output');
@@ -15,8 +18,10 @@ let nextStep;
15
18
  let finish;
16
19
  let next;
17
20
  let registeredVariables = {};
21
+ let aiAssistant;
18
22
  /**
19
23
  * Pauses test execution and starts interactive shell
24
+ * @param {Object<string, *>} [passedObject]
20
25
  */
21
26
  const pause = function (passedObject = {}) {
22
27
  if (store.dryRun) return;
@@ -39,12 +44,22 @@ function pauseSession(passedObject = {}) {
39
44
  let vars = Object.keys(registeredVariables).join(', ');
40
45
  if (vars) vars = `(vars: ${vars})`;
41
46
 
47
+ aiAssistant = AiAssistant.getInstance();
48
+
42
49
  output.print(colors.yellow(' Interactive shell started'));
43
50
  output.print(colors.yellow(' Use JavaScript syntax to try steps in action'));
44
51
  output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`));
45
52
  output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
46
53
  output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`));
47
54
  output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`));
55
+
56
+ if (aiAssistant.isEnabled) {
57
+ output.print(colors.blue(` ${colors.bold('OpenAI is enabled! (experimental)')} Write what you want and make OpenAI run it`));
58
+ output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to OpenAI'));
59
+ output.print(colors.blue(' Ideas: ask it to fill forms for you or to click'));
60
+ } else {
61
+ output.print(colors.blue(` Enable OpenAI assistant by setting ${colors.bold('OPENAI_API_KEY')} env variable`));
62
+ }
48
63
  }
49
64
  rl = readline.createInterface(process.stdin, process.stdout, completer);
50
65
 
@@ -54,15 +69,16 @@ function pauseSession(passedObject = {}) {
54
69
  });
55
70
  return new Promise(((resolve) => {
56
71
  finish = resolve;
72
+ // eslint-disable-next-line
57
73
  return askForStep();
58
74
  }));
59
75
  }
60
76
 
61
77
  /* eslint-disable */
62
- function parseInput(cmd) {
78
+ async function parseInput(cmd) {
63
79
  rl.pause();
64
80
  next = false;
65
- store.debugMode = false;
81
+ recorder.session.start('pause');
66
82
  if (cmd === '') next = true;
67
83
  if (!cmd || cmd === 'resume' || cmd === 'exit') {
68
84
  finish();
@@ -74,37 +90,79 @@ function parseInput(cmd) {
74
90
  for (const k of Object.keys(registeredVariables)) {
75
91
  eval(`var ${k} = registeredVariables['${k}'];`); // eslint-disable-line no-eval
76
92
  }
77
- store.debugMode = true;
93
+
94
+ let executeCommand = Promise.resolve();
95
+
96
+ const getCmd = () => {
97
+ debug('Command:', cmd)
98
+ return cmd;
99
+ };
100
+
78
101
  let isCustomCommand = false;
79
102
  let lastError = null;
103
+ let isAiCommand = false;
104
+ let $res;
80
105
  try {
106
+ // eslint-disable-next-line
81
107
  const locate = global.locate; // enable locate in this context
108
+ // eslint-disable-next-line
82
109
  const I = container.support('I');
83
110
  if (cmd.trim().startsWith('=>')) {
84
111
  isCustomCommand = true;
85
112
  cmd = cmd.trim().substring(2, cmd.length);
113
+ } else if (aiAssistant.isEnabled && !cmd.match(/^\w+\(/) && cmd.includes(' ')) {
114
+ const currentOutputLevel = output.level();
115
+ output.level(0);
116
+ const res = I.grabSource();
117
+ isAiCommand = true;
118
+ executeCommand = executeCommand.then(async () => {
119
+ try {
120
+ const html = await res;
121
+ await aiAssistant.setHtmlContext(html);
122
+ } catch (err) {
123
+ output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
124
+ return;
125
+ } finally {
126
+ output.level(currentOutputLevel);
127
+ }
128
+ // aiAssistant.mockResponse("```js\nI.click('Sign in');\n```");
129
+ const spinner = ora("Processing OpenAI request...").start();
130
+ cmd = await aiAssistant.writeSteps(cmd);
131
+ spinner.stop();
132
+ output.print('');
133
+ output.print(colors.blue(aiAssistant.getResponse()));
134
+ output.print('');
135
+ return cmd;
136
+ })
86
137
  } else {
87
138
  cmd = `I.${cmd}`;
88
139
  }
89
- const executeCommand = eval(cmd); // eslint-disable-line no-eval
90
-
91
- const result = executeCommand instanceof Promise ? executeCommand : Promise.resolve(executeCommand);
92
- result.then((val) => {
93
- if (isCustomCommand) {
94
- console.log(val);
95
- return;
96
- }
97
- if (cmd.startsWith('I.see') || cmd.startsWith('I.dontSee')) {
98
- output.print(output.styles.success(' OK '), cmd);
99
- return;
100
- }
101
- if (cmd.startsWith('I.grab')) {
102
- output.print(output.styles.debug(val));
103
- }
140
+ executeCommand = executeCommand.then(async () => {
141
+ const cmd = getCmd();
142
+ if (!cmd) return;
143
+ return eval(cmd); // eslint-disable-line no-eval
104
144
  }).catch((err) => {
145
+ debug(err);
146
+ if (isAiCommand) return;
105
147
  if (!lastError) output.print(output.styles.error(' ERROR '), err.message);
148
+ debug(err.stack)
149
+
106
150
  lastError = err.message;
107
- });
151
+ })
152
+
153
+ const val = await executeCommand;
154
+
155
+ if (isCustomCommand) {
156
+ if (val !== undefined) console.log('Result', '$res=', val); // eslint-disable-line
157
+ $res = val;
158
+ }
159
+
160
+ if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
161
+ output.print(output.styles.success(' OK '), cmd);
162
+ }
163
+ if (cmd?.startsWith('I.grab')) {
164
+ output.print(output.styles.debug(val));
165
+ }
108
166
 
109
167
  history.push(cmd); // add command to history when successful
110
168
  } catch (err) {
@@ -117,6 +175,7 @@ function parseInput(cmd) {
117
175
  // pop latest command from history because it failed
118
176
  history.pop();
119
177
 
178
+ if (isAiCommand) return;
120
179
  if (!lastError) output.print(output.styles.error(' FAIL '), msg);
121
180
  lastError = err.message;
122
181
  });
@@ -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
 
@@ -0,0 +1,67 @@
1
+ const Container = require('../container');
2
+ const recorder = require('../recorder');
3
+ const event = require('../event');
4
+ const supportedHelpers = require('./standardActingHelpers');
5
+ const { scanForErrorMessages } = require('../html');
6
+ const { output } = require('..');
7
+
8
+ const defaultConfig = {
9
+ errorClasses: ['error', 'warning', 'alert', 'danger'],
10
+ };
11
+
12
+ /**
13
+ * Prints errors found in HTML code after each failed test.
14
+ *
15
+ * It scans HTML and searches for elements with error classes.
16
+ * If an element found prints a text from it to console and adds as artifact to the test.
17
+ *
18
+ * Enable this plugin in config:
19
+ *
20
+ * ```js
21
+ * plugins: {
22
+ * debugErrors: {
23
+ * enabled: true,
24
+ * }
25
+ * ```
26
+ *
27
+ * Additional config options:
28
+ *
29
+ * * `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`)
30
+ *
31
+ */
32
+ module.exports = function (config = {}) {
33
+ const helpers = Container.helpers();
34
+ let helper;
35
+
36
+ config = Object.assign(defaultConfig, config);
37
+
38
+ for (const helperName of supportedHelpers) {
39
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
40
+ helper = helpers[helperName];
41
+ }
42
+ }
43
+
44
+ if (!helper) return; // no helpers for screenshot
45
+
46
+ event.dispatcher.on(event.test.failed, (test) => {
47
+ recorder.add('HTML snapshot failed test', async () => {
48
+ try {
49
+ const currentOutputLevel = output.level();
50
+ output.level(0);
51
+ const html = await helper.grabHTMLFrom('body');
52
+ output.level(currentOutputLevel);
53
+
54
+ if (!html) return;
55
+
56
+ const errors = scanForErrorMessages(html, config.errorClasses);
57
+ if (errors.length) {
58
+ output.debug('Detected errors in HTML code');
59
+ errors.forEach((error) => output.debug(error));
60
+ test.artifacts.errors = errors;
61
+ }
62
+ } catch (err) {
63
+ // not really needed
64
+ }
65
+ });
66
+ });
67
+ };
@@ -2,20 +2,18 @@ const { faker } = require('@faker-js/faker');
2
2
  const transform = require('../transform');
3
3
 
4
4
  /**
5
- * Use the [faker.js](https://www.npmjs.com/package/faker) package to generate fake data inside examples on your gherkin tests
6
- *
7
- * ![Faker.js](https://raw.githubusercontent.com/Marak/faker.js/master/logo.png)
5
+ * Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests
8
6
  *
9
7
  * #### Usage
10
8
  *
11
- * To start please install `faker.js` package
9
+ * To start please install `@faker-js/faker` package
12
10
  *
13
11
  * ```
14
- * npm install -D faker
12
+ * npm install -D @faker-js/faker
15
13
  * ```
16
14
  *
17
15
  * ```
18
- * yarn add -D faker
16
+ * yarn add -D @faker-js/faker
19
17
  * ```
20
18
  *
21
19
  * Add this plugin to config file:
@@ -0,0 +1,209 @@
1
+ const debug = require('debug')('codeceptjs:heal');
2
+ const colors = require('chalk');
3
+ const Container = require('../container');
4
+ const AiAssistant = require('../ai');
5
+ const recorder = require('../recorder');
6
+ const event = require('../event');
7
+ const output = require('../output');
8
+ const supportedHelpers = require('./standardActingHelpers');
9
+
10
+ const defaultConfig = {
11
+ healTries: 1,
12
+ healLimit: 2,
13
+ healSteps: [
14
+ 'click',
15
+ 'fillField',
16
+ 'appendField',
17
+ 'selectOption',
18
+ 'attachFile',
19
+ 'checkOption',
20
+ 'uncheckOption',
21
+ 'doubleClick',
22
+ ],
23
+ };
24
+
25
+ /**
26
+ * Self-healing tests with OpenAI.
27
+ *
28
+ * This plugin is experimental and requires OpenAI API key.
29
+ *
30
+ * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
31
+ *
32
+ * ```js
33
+ * plugins: {
34
+ * heal: {
35
+ * enabled: true,
36
+ * }
37
+ * }
38
+ * ```
39
+ *
40
+ * More config options are available:
41
+ *
42
+ * * `healLimit` - how many steps can be healed in a single test (default: 2)
43
+ * * `healSteps` - which steps can be healed (default: all steps that interact with UI, see list below)
44
+ *
45
+ * Steps to heal:
46
+ *
47
+ * * `click`
48
+ * * `fillField`
49
+ * * `appendField`
50
+ * * `selectOption`
51
+ * * `attachFile`
52
+ * * `checkOption`
53
+ * * `uncheckOption`
54
+ * * `doubleClick`
55
+ *
56
+ */
57
+ module.exports = function (config = {}) {
58
+ const aiAssistant = AiAssistant.getInstance();
59
+
60
+ let currentTest = null;
61
+ let currentStep = null;
62
+ let healedSteps = 0;
63
+ let caughtError;
64
+ let healTries = 0;
65
+ let isHealing = false;
66
+
67
+ const healSuggestions = [];
68
+
69
+ config = Object.assign(defaultConfig, config);
70
+
71
+ event.dispatcher.on(event.test.before, (test) => {
72
+ currentTest = test;
73
+ healedSteps = 0;
74
+ caughtError = null;
75
+ });
76
+
77
+ event.dispatcher.on(event.step.started, step => currentStep = step);
78
+
79
+ event.dispatcher.on(event.step.after, (step) => {
80
+ if (isHealing) return;
81
+ const store = require('../store');
82
+ if (store.debugMode) return;
83
+ recorder.catchWithoutStop(async (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
+ }
91
+ if (!currentStep) throw err;
92
+ if (!config.healSteps.includes(currentStep.name)) throw err;
93
+ const test = currentTest;
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
+
103
+ if (healedSteps >= config.healLimit) {
104
+ output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
105
+ output.print('Entire flow can be broken, please check it manually');
106
+ output.print('or increase healing limit in heal plugin config');
107
+
108
+ throw err;
109
+ }
110
+
111
+ recorder.session.start('heal');
112
+ const helpers = Container.helpers();
113
+ let helper;
114
+
115
+ for (const helperName of supportedHelpers) {
116
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
117
+ helper = helpers[helperName];
118
+ }
119
+ }
120
+
121
+ if (!helper) throw err; // no helpers for html
122
+
123
+ const step = test.steps[test.steps.length - 1];
124
+ debug('Self-healing started', step.toCode());
125
+
126
+ const currentOutputLevel = output.level();
127
+ output.level(0);
128
+ const html = await helper.grabHTMLFrom('body');
129
+ output.level(currentOutputLevel);
130
+
131
+ if (!html) throw err;
132
+
133
+ healTries++;
134
+ await aiAssistant.setHtmlContext(html);
135
+ await tryToHeal(step, err);
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;
144
+ });
145
+ });
146
+
147
+ event.dispatcher.on(event.all.result, () => {
148
+ if (!healSuggestions.length) return;
149
+
150
+ const { print } = output;
151
+
152
+ print('');
153
+ print('===================');
154
+ print(colors.bold.green('Self-Healing Report:'));
155
+
156
+ print(`${colors.bold(healSuggestions.length)} step(s) were healed by AI`);
157
+
158
+ let i = 1;
159
+ print('');
160
+ print('Suggested changes:');
161
+ print('');
162
+
163
+ for (const suggestion of healSuggestions) {
164
+ print(`${i}. To fix ${colors.bold.blue(suggestion.test.title)}`);
165
+ print('Replace the failed code with:');
166
+ print(colors.red(`- ${suggestion.step.toCode()}`));
167
+ print(colors.green(`+ ${suggestion.snippet}`));
168
+ print(suggestion.step.line());
169
+ print('');
170
+ i++;
171
+ }
172
+ });
173
+
174
+ async function tryToHeal(failedStep, err) {
175
+ output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
176
+
177
+ const codeSnippets = await aiAssistant.healFailedStep(failedStep, err, currentTest);
178
+
179
+ output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
180
+ const I = Container.support('I'); // eslint-disable-line
181
+
182
+ for (const codeSnippet of codeSnippets) {
183
+ try {
184
+ debug('Executing', codeSnippet);
185
+ recorder.catch((e) => {
186
+ console.log(e);
187
+ });
188
+ await eval(codeSnippet); // eslint-disable-line
189
+
190
+ healSuggestions.push({
191
+ test: currentTest,
192
+ step: failedStep,
193
+ snippet: codeSnippet,
194
+ });
195
+
196
+ recorder.add('healed', () => output.print(colors.bold.green(' Code healed successfully')));
197
+ healedSteps++;
198
+ return;
199
+ } catch (err) {
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')));
203
+ }
204
+ }
205
+
206
+ output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
207
+ }
208
+ return recorder.promise();
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(() => {
@@ -75,7 +75,8 @@ module.exports = function (config) {
75
75
  event.dispatcher.on(event.test.failed, (test) => {
76
76
  recorder.add('screenshot of failed test', async () => {
77
77
  let fileName = clearString(test.title);
78
- // This prevent data driven to be included in the failed screenshot file name
78
+ const dataType = 'image/png';
79
+ // This prevents data driven to be included in the failed screenshot file name
79
80
  if (fileName.indexOf('{') !== -1) {
80
81
  fileName = fileName.substr(0, (fileName.indexOf('{') - 3)).trim();
81
82
  }
@@ -106,7 +107,15 @@ module.exports = function (config) {
106
107
 
107
108
  const allureReporter = Container.plugins('allure');
108
109
  if (allureReporter) {
109
- allureReporter.addAttachment('Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), 'image/png');
110
+ allureReporter.addAttachment('Main session - Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), dataType);
111
+
112
+ if (helper.activeSessionName) {
113
+ for (const sessionName in helper.sessionPages) {
114
+ const screenshotFileName = `${sessionName}_${fileName}`;
115
+ test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName);
116
+ allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType);
117
+ }
118
+ }
110
119
  }
111
120
 
112
121
  const cucumberReporter = Container.plugins('cucumberJsonReporter');
@@ -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.