codeceptjs 3.4.1 → 3.5.0

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 (69) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +9 -7
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +246 -0
  5. package/docs/build/Appium.js +47 -7
  6. package/docs/build/JSONResponse.js +4 -4
  7. package/docs/build/Nightmare.js +3 -1
  8. package/docs/build/OpenAI.js +122 -0
  9. package/docs/build/Playwright.js +193 -45
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +45 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +3 -1
  14. package/docs/build/WebDriver.js +30 -5
  15. package/docs/changelog.md +65 -0
  16. package/docs/helpers/Appium.md +152 -147
  17. package/docs/helpers/JSONResponse.md +4 -4
  18. package/docs/helpers/Nightmare.md +2 -0
  19. package/docs/helpers/OpenAI.md +70 -0
  20. package/docs/helpers/Playwright.md +194 -152
  21. package/docs/helpers/Puppeteer.md +6 -0
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +2 -0
  24. package/docs/helpers/WebDriver.md +10 -4
  25. package/docs/mobile.md +49 -2
  26. package/docs/parallel.md +56 -0
  27. package/docs/plugins.md +87 -33
  28. package/docs/secrets.md +6 -0
  29. package/docs/tutorial.md +2 -2
  30. package/docs/webapi/appendField.mustache +2 -0
  31. package/docs/webapi/type.mustache +3 -0
  32. package/lib/ai.js +171 -0
  33. package/lib/cli.js +1 -1
  34. package/lib/codecept.js +4 -0
  35. package/lib/command/dryRun.js +9 -1
  36. package/lib/command/generate.js +46 -3
  37. package/lib/command/init.js +13 -1
  38. package/lib/command/interactive.js +15 -1
  39. package/lib/command/run-workers.js +2 -1
  40. package/lib/container.js +13 -3
  41. package/lib/helper/Appium.js +45 -7
  42. package/lib/helper/JSONResponse.js +4 -4
  43. package/lib/helper/Nightmare.js +1 -1
  44. package/lib/helper/OpenAI.js +122 -0
  45. package/lib/helper/Playwright.js +190 -38
  46. package/lib/helper/Protractor.js +1 -1
  47. package/lib/helper/Puppeteer.js +40 -12
  48. package/lib/helper/REST.js +15 -5
  49. package/lib/helper/TestCafe.js +1 -1
  50. package/lib/helper/WebDriver.js +25 -5
  51. package/lib/helper/scripts/highlightElement.js +20 -0
  52. package/lib/html.js +258 -0
  53. package/lib/listener/retry.js +2 -1
  54. package/lib/pause.js +73 -17
  55. package/lib/plugin/debugErrors.js +67 -0
  56. package/lib/plugin/fakerTransform.js +4 -6
  57. package/lib/plugin/heal.js +179 -0
  58. package/lib/plugin/screenshotOnFail.js +11 -2
  59. package/lib/recorder.js +4 -4
  60. package/lib/secret.js +5 -4
  61. package/lib/step.js +6 -1
  62. package/lib/ui.js +4 -3
  63. package/lib/utils.js +4 -0
  64. package/lib/workers.js +57 -9
  65. package/package.json +25 -13
  66. package/translations/ja-JP.js +9 -9
  67. package/typings/index.d.ts +43 -9
  68. package/typings/promiseBasedTypes.d.ts +124 -24
  69. package/typings/types.d.ts +138 -30
@@ -34,7 +34,8 @@ const config = {};
34
34
  * endpoint: 'http://site.com/api',
35
35
  * prettyPrintJson: true,
36
36
  * onRequest: (request) => {
37
- * request.headers.auth = '123';
37
+ * request.headers.auth = '123';
38
+ * }
38
39
  * }
39
40
  * }
40
41
  *}
@@ -136,6 +137,15 @@ class REST extends Helper {
136
137
  request.auth = this.headers.auth;
137
138
  }
138
139
 
140
+ if (typeof request.data === 'object') {
141
+ const returnedValue = {};
142
+ for (const [key, value] of Object.entries(request.data)) {
143
+ returnedValue[key] = value;
144
+ if (value instanceof Secret) returnedValue[key] = value.getMasked();
145
+ }
146
+ _debugRequest.data = returnedValue;
147
+ }
148
+
139
149
  if (request.data instanceof Secret) {
140
150
  _debugRequest.data = '*****';
141
151
  request.data = (typeof request.data === 'object' && !(request.data instanceof Secret)) ? { ...request.data.toString() } : request.data.toString();
@@ -198,7 +208,7 @@ class REST extends Helper {
198
208
  * ```
199
209
  *
200
210
  * @param {*} url
201
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
211
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
202
212
  *
203
213
  * @returns {Promise<*>} response
204
214
  */
@@ -222,8 +232,8 @@ class REST extends Helper {
222
232
  * ```
223
233
  *
224
234
  * @param {*} url
225
- * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
226
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
235
+ * @param {*} [payload={}] - the payload to be sent. By default, it is sent as an empty object
236
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
227
237
  *
228
238
  * @returns {Promise<*>} response
229
239
  */
@@ -317,7 +327,7 @@ class REST extends Helper {
317
327
  * ```
318
328
  *
319
329
  * @param {*} url
320
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
330
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
321
331
  *
322
332
  * @returns {Promise<*>} response
323
333
  */
@@ -410,7 +410,7 @@ class TestCafe extends Helper {
410
410
  const el = await els.nth(0);
411
411
 
412
412
  return this.t
413
- .typeText(el, value, { replace: false })
413
+ .typeText(el, value.toString(), { replace: false })
414
414
  .catch(mapError);
415
415
  }
416
416
 
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
 
7
7
  const Helper = require('@codeceptjs/helper');
8
+ const crypto = require('crypto');
8
9
  const stringIncludes = require('../assert/include').includes;
9
10
  const { urlEquals, equals } = require('../assert/equal');
10
11
  const { debug } = require('../output');
@@ -27,6 +28,8 @@ const {
27
28
  const ElementNotFound = require('./errors/ElementNotFound');
28
29
  const ConnectionRefused = require('./errors/ConnectionRefused');
29
30
  const Locator = require('../locator');
31
+ const { highlightElement } = require('./scripts/highlightElement');
32
+ const store = require('../store');
30
33
 
31
34
  const SHADOW = 'shadow';
32
35
  const webRoot = 'body';
@@ -39,7 +42,7 @@ const webRoot = 'body';
39
42
  * @typedef WebDriverConfig
40
43
  * @type {object}
41
44
  * @prop {string} url - base url of website to be tested.
42
- * @prop {string} browser browser in which to perform testing.
45
+ * @prop {string} browser - Browser in which to perform testing.
43
46
  * @prop {string} [basicAuth] - (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
44
47
  * @prop {string} [host=localhost] - WebDriver host to connect.
45
48
  * @prop {number} [port=4444] - WebDriver port to connect.
@@ -57,6 +60,7 @@ const webRoot = 'body';
57
60
  * @prop {object} [desiredCapabilities] Selenium's [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities).
58
61
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
59
62
  * @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
63
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
60
64
  */
61
65
  const config = {};
62
66
 
@@ -822,7 +826,7 @@ class WebDriver extends Helper {
822
826
  }
823
827
 
824
828
  /**
825
- * Find a checkbox by providing human readable text:
829
+ * Find a checkbox by providing human-readable text:
826
830
  *
827
831
  * ```js
828
832
  * this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ...
@@ -835,7 +839,7 @@ class WebDriver extends Helper {
835
839
  }
836
840
 
837
841
  /**
838
- * Find a clickable element by providing human readable text:
842
+ * Find a clickable element by providing human-readable text:
839
843
  *
840
844
  * ```js
841
845
  * const els = await this.helpers.WebDriver._locateClickable('Next page');
@@ -850,7 +854,7 @@ class WebDriver extends Helper {
850
854
  }
851
855
 
852
856
  /**
853
- * Find field elements by providing human readable text:
857
+ * Find field elements by providing human-readable text:
854
858
  *
855
859
  * ```js
856
860
  * this.helpers['WebDriver']._locateFields('Your email').then // ...
@@ -914,6 +918,7 @@ class WebDriver extends Helper {
914
918
  assertElementExists(res, locator, 'Clickable element');
915
919
  }
916
920
  const elem = usingFirstElement(res);
921
+ highlightActiveElement.call(this, elem);
917
922
  return this.browser[clickMethod](getElementId(elem));
918
923
  }
919
924
 
@@ -932,6 +937,7 @@ class WebDriver extends Helper {
932
937
  assertElementExists(res, locator, 'Clickable element');
933
938
  }
934
939
  const elem = usingFirstElement(res);
940
+ highlightActiveElement.call(this, elem);
935
941
 
936
942
  return this.executeScript((el) => {
937
943
  if (document.activeElement instanceof HTMLElement) {
@@ -959,6 +965,7 @@ class WebDriver extends Helper {
959
965
  }
960
966
 
961
967
  const elem = usingFirstElement(res);
968
+ highlightActiveElement.call(this, elem);
962
969
  return elem.doubleClick();
963
970
  }
964
971
 
@@ -1024,6 +1031,7 @@ class WebDriver extends Helper {
1024
1031
  const res = await findFields.call(this, field);
1025
1032
  assertElementExists(res, field, 'Field');
1026
1033
  const elem = usingFirstElement(res);
1034
+ highlightActiveElement.call(this, elem);
1027
1035
  return elem.setValue(value.toString());
1028
1036
  }
1029
1037
 
@@ -1035,7 +1043,8 @@ class WebDriver extends Helper {
1035
1043
  const res = await findFields.call(this, field);
1036
1044
  assertElementExists(res, field, 'Field');
1037
1045
  const elem = usingFirstElement(res);
1038
- return elem.addValue(value);
1046
+ highlightActiveElement.call(this, elem);
1047
+ return elem.addValue(value.toString());
1039
1048
  }
1040
1049
 
1041
1050
  /**
@@ -1046,6 +1055,7 @@ class WebDriver extends Helper {
1046
1055
  const res = await findFields.call(this, field);
1047
1056
  assertElementExists(res, field, 'Field');
1048
1057
  const elem = usingFirstElement(res);
1058
+ highlightActiveElement.call(this, elem);
1049
1059
  return elem.clearValue(getElementId(elem));
1050
1060
  }
1051
1061
 
@@ -1056,6 +1066,7 @@ class WebDriver extends Helper {
1056
1066
  const res = await findFields.call(this, select);
1057
1067
  assertElementExists(res, select, 'Selectable field');
1058
1068
  const elem = usingFirstElement(res);
1069
+ highlightActiveElement.call(this, elem);
1059
1070
 
1060
1071
  if (!Array.isArray(option)) {
1061
1072
  option = [option];
@@ -1122,6 +1133,7 @@ class WebDriver extends Helper {
1122
1133
  assertElementExists(res, field, 'Checkable');
1123
1134
  const elem = usingFirstElement(res);
1124
1135
  const elementId = getElementId(elem);
1136
+ highlightActiveElement.call(this, elem);
1125
1137
 
1126
1138
  const isSelected = await this.browser.isElementSelected(elementId);
1127
1139
  if (isSelected) return Promise.resolve(true);
@@ -1141,6 +1153,7 @@ class WebDriver extends Helper {
1141
1153
  assertElementExists(res, field, 'Checkable');
1142
1154
  const elem = usingFirstElement(res);
1143
1155
  const elementId = getElementId(elem);
1156
+ highlightActiveElement.call(this, elem);
1144
1157
 
1145
1158
  const isSelected = await this.browser.isElementSelected(elementId);
1146
1159
  if (!isSelected) return Promise.resolve(true);
@@ -1879,6 +1892,7 @@ class WebDriver extends Helper {
1879
1892
  */
1880
1893
  async type(keys, delay = null) {
1881
1894
  if (!Array.isArray(keys)) {
1895
+ keys = keys.toString();
1882
1896
  keys = keys.split('');
1883
1897
  }
1884
1898
  if (delay) {
@@ -2873,6 +2887,12 @@ function isModifierKey(key) {
2873
2887
  return unicodeModifierKeys.includes(key);
2874
2888
  }
2875
2889
 
2890
+ function highlightActiveElement(element) {
2891
+ if (!this.options.enableHighlight && !store.debugMode) return;
2892
+
2893
+ highlightElement(element, this.browser);
2894
+ }
2895
+
2876
2896
  function prepareLocateFn(context) {
2877
2897
  if (!context) return this._locate.bind(this);
2878
2898
  return (l) => {
@@ -0,0 +1,20 @@
1
+ module.exports.highlightElement = (element, context) => {
2
+ const clientSideHighlightFn = el => {
3
+ const style = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)';
4
+ const prevStyle = el.style.boxShadow;
5
+ el.style.boxShadow = style;
6
+ setTimeout(() => el.style.boxShadow = prevStyle, 2000);
7
+ };
8
+
9
+ try {
10
+ // Playwright, Puppeteer
11
+ context.evaluate(clientSideHighlightFn, element);
12
+ } catch (e) {
13
+ // WebDriver
14
+ try {
15
+ context.execute(clientSideHighlightFn, element);
16
+ } catch (err) {
17
+ // ignore
18
+ }
19
+ }
20
+ };
package/lib/html.js ADDED
@@ -0,0 +1,258 @@
1
+ const { parse, serialize } = require('parse5');
2
+ const { minify } = require('html-minifier');
3
+
4
+ function minifyHtml(html) {
5
+ return minify(html, {
6
+ collapseWhitespace: true,
7
+ removeComments: true,
8
+ removeEmptyAttributes: true,
9
+ removeRedundantAttributes: true,
10
+ removeScriptTypeAttributes: true,
11
+ removeStyleLinkTypeAttributes: true,
12
+ collapseBooleanAttributes: true,
13
+ useShortDoctype: true,
14
+ }).toString();
15
+ }
16
+
17
+ const defaultHtmlOpts = {
18
+ interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
19
+ textElements: ['label', 'h1', 'h2'],
20
+ allowedAttrs: ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'role'],
21
+ allowedRoles: ['button', 'checkbox', 'search', 'textbox', 'tab'],
22
+ };
23
+
24
+ function removeNonInteractiveElements(html, opts = {}) {
25
+ opts = { ...defaultHtmlOpts, ...opts };
26
+ const {
27
+ interactiveElements,
28
+ textElements,
29
+ allowedAttrs,
30
+ allowedRoles,
31
+ } = opts;
32
+
33
+ // Parse the HTML into a document tree
34
+ const document = parse(html);
35
+
36
+ const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/;
37
+ // Array to store interactive elements
38
+ const removeElements = ['path', 'script'];
39
+
40
+ function isFilteredOut(node) {
41
+ if (removeElements.includes(node.nodeName)) return true;
42
+ if (node.attrs) {
43
+ if (node.attrs.find(attr => attr.name === 'role' && attr.value === 'tooltip')) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ // Function to check if an element is interactive
49
+ function isInteractive(element) {
50
+ if (element.nodeName === 'input' && element.attrs.find(attr => attr.name === 'type' && attr.value === 'hidden')) return false;
51
+ if (interactiveElements.includes(element.nodeName)) return true;
52
+ if (element.attrs) {
53
+ if (element.attrs.find(attr => attr.name === 'contenteditable')) return true;
54
+ if (element.attrs.find(attr => attr.name === 'tabindex')) return true;
55
+ const role = element.attrs.find(attr => attr.name === 'role');
56
+ if (role && allowedRoles.includes(role.value)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function hasMeaningfulText(node) {
62
+ if (textElements.includes(node.nodeName)) return true;
63
+ return false;
64
+ }
65
+
66
+ function hasInteractiveDescendant(node) {
67
+ if (!node.childNodes) return false;
68
+ let result = false;
69
+
70
+ for (const childNode of node.childNodes) {
71
+ if (isInteractive(childNode) || hasMeaningfulText(childNode)) return true;
72
+ result = result || hasInteractiveDescendant(childNode);
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ // Function to remove non-interactive elements recursively
79
+ function removeNonInteractive(node) {
80
+ if (node.nodeName !== '#document') {
81
+ const parent = node.parentNode;
82
+ const index = parent.childNodes.indexOf(node);
83
+
84
+ if (isFilteredOut(node)) {
85
+ parent.childNodes.splice(index, 1);
86
+ return true;
87
+ }
88
+
89
+ // keep texts for interactive elements
90
+ if ((isInteractive(parent) || hasMeaningfulText(parent)) && node.nodeName === '#text') {
91
+ node.value = node.value.trim().slice(0, 200);
92
+ if (!node.value) return false;
93
+ return true;
94
+ }
95
+
96
+ if (
97
+ // if parent is interactive, we may need child element to match
98
+ !isInteractive(parent)
99
+ && !isInteractive(node)
100
+ && !hasInteractiveDescendant(node)
101
+ && !hasMeaningfulText(node)) {
102
+ parent.childNodes.splice(index, 1);
103
+ return true;
104
+ }
105
+ }
106
+
107
+ if (node.attrs) {
108
+ // Filter and keep allowed attributes, accessibility attributes
109
+ node.attrs = node.attrs.filter(attr => {
110
+ const { name, value } = attr;
111
+ if (name === 'class') {
112
+ // Remove classes containing digits
113
+ attr.value = value.split(' ')
114
+ // remove classes containing digits/
115
+ .filter(className => !/\d/.test(className))
116
+ // remove popular trash classes
117
+ .filter(className => !className.match(trashHtmlClasses))
118
+ // remove classes with : and __ in them
119
+ .filter(className => !className.match(/(:|__)/))
120
+ .join(' ');
121
+ }
122
+
123
+ return allowedAttrs.includes(name);
124
+ });
125
+ }
126
+
127
+ if (node.childNodes) {
128
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
129
+ const childNode = node.childNodes[i];
130
+ removeNonInteractive(childNode);
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+
136
+ // Remove non-interactive elements starting from the root element
137
+ removeNonInteractive(document);
138
+
139
+ // Serialize the modified document tree back to HTML
140
+ const serializedHTML = serialize(document);
141
+
142
+ return serializedHTML;
143
+ }
144
+
145
+ function scanForErrorMessages(html, errorClasses = []) {
146
+ // Parse the HTML into a document tree
147
+ const document = parse(html);
148
+
149
+ // Array to store error messages
150
+ const errorMessages = [];
151
+
152
+ // Function to recursively scan for error classes and messages
153
+ function scanErrors(node) {
154
+ if (node.attrs) {
155
+ const classAttr = node.attrs.find(attr => attr.name === 'class');
156
+ if (classAttr && classAttr.value) {
157
+ const classNameChunks = classAttr.value.split(' ');
158
+ const errorClassFound = errorClasses.some(errorClass => classNameChunks.includes(errorClass));
159
+ if (errorClassFound && node.childNodes) {
160
+ const errorMessage = sanitizeTextContent(node);
161
+ errorMessages.push(errorMessage);
162
+ }
163
+ }
164
+ }
165
+
166
+ if (node.childNodes) {
167
+ for (const childNode of node.childNodes) {
168
+ scanErrors(childNode);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Start scanning for error classes and messages from the root element
174
+ scanErrors(document);
175
+
176
+ return errorMessages;
177
+ }
178
+
179
+ function sanitizeTextContent(node) {
180
+ if (node.nodeName === '#text') {
181
+ return node.value.trim();
182
+ }
183
+
184
+ let sanitizedText = '';
185
+
186
+ if (node.childNodes) {
187
+ for (const childNode of node.childNodes) {
188
+ sanitizedText += sanitizeTextContent(childNode);
189
+ }
190
+ }
191
+
192
+ return sanitizedText;
193
+ }
194
+
195
+ function buildPath(node, path = '') {
196
+ const tag = node.nodeName;
197
+ let attributes = '';
198
+
199
+ if (node.attrs) {
200
+ attributes = node.attrs
201
+ .map(attr => `${attr.name}="${attr.value}"`)
202
+ .join(' ');
203
+ }
204
+
205
+ if (!tag.startsWith('#') && tag !== 'body' && tag !== 'html') {
206
+ path += `<${node.nodeName}${node.attrs ? ` ${attributes}` : ''}>`;
207
+ }
208
+
209
+ if (!node.childNodes) return path;
210
+
211
+ const children = node.childNodes.filter(child => !child.nodeName.startsWith('#'));
212
+
213
+ if (children.length) {
214
+ return buildPath(children[children.length - 1], path);
215
+ }
216
+ return path;
217
+ }
218
+
219
+ function splitByChunks(text, chunkSize) {
220
+ chunkSize -= 20;
221
+ const chunks = [];
222
+ for (let i = 0; i < text.length; i += chunkSize) {
223
+ chunks.push(text.slice(i, i + chunkSize));
224
+ }
225
+
226
+ const regex = /<\s*\w+(?:\s+\w+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]+)))*\s*$/;
227
+
228
+ // append tag to chunk if it was split out
229
+ for (const index in chunks) {
230
+ const nextIndex = parseInt(index, 10) + 1;
231
+ if (!chunks[nextIndex]) break;
232
+
233
+ const currentChunk = chunks[index];
234
+ const nextChunk = chunks[nextIndex];
235
+
236
+ const lastTag = currentChunk.match(regex);
237
+ if (lastTag) {
238
+ chunks[nextIndex] = lastTag[0] + nextChunk;
239
+ }
240
+
241
+ const path = buildPath(parse(currentChunk));
242
+ if (path) {
243
+ chunks[nextIndex] = path + chunks[nextIndex];
244
+ }
245
+
246
+ if (chunks[nextIndex].includes('<html')) continue;
247
+ chunks[nextIndex] = `<html><body>${chunks[nextIndex]}</body></html>`;
248
+ }
249
+
250
+ return chunks.map(chunk => chunk.trim());
251
+ }
252
+
253
+ module.exports = {
254
+ scanForErrorMessages,
255
+ removeNonInteractiveElements,
256
+ splitByChunks,
257
+ minifyHtml,
258
+ };
@@ -44,6 +44,7 @@ module.exports = function () {
44
44
  if (!retryConfig) return;
45
45
 
46
46
  if (Number.isInteger(+retryConfig)) {
47
+ if (test.retries() === -1) test.retries(retryConfig);
47
48
  return;
48
49
  }
49
50
 
@@ -59,7 +60,7 @@ module.exports = function () {
59
60
  }
60
61
 
61
62
  if (config.Scenario) {
62
- if (isNotSet(test.retries())) test.retries(config.Scenario);
63
+ if (test.retries() === -1) test.retries(config.Scenario);
63
64
  output.log(`Retries: ${config.Scenario}`);
64
65
  }
65
66
  }
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,6 +18,8 @@ let nextStep;
15
18
  let finish;
16
19
  let next;
17
20
  let registeredVariables = {};
21
+ const aiAssistant = new AiAssistant();
22
+
18
23
  /**
19
24
  * Pauses test execution and starts interactive shell
20
25
  */
@@ -45,6 +50,14 @@ function pauseSession(passedObject = {}) {
45
50
  output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
46
51
  output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`));
47
52
  output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`));
53
+
54
+ if (aiAssistant.isEnabled) {
55
+ output.print(colors.blue(` ${colors.bold('OpenAI is enabled! (experimental)')} Write what you want and make OpenAI run it`));
56
+ output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to OpenAI'));
57
+ output.print(colors.blue(' Ideas: ask it to fill forms for you or to click'));
58
+ } else {
59
+ output.print(colors.blue(` Enable OpenAI assistant by setting ${colors.bold('OPENAI_API_KEY')} env variable`));
60
+ }
48
61
  }
49
62
  rl = readline.createInterface(process.stdin, process.stdout, completer);
50
63
 
@@ -59,9 +72,10 @@ function pauseSession(passedObject = {}) {
59
72
  }
60
73
 
61
74
  /* eslint-disable */
62
- function parseInput(cmd) {
75
+ async function parseInput(cmd) {
63
76
  rl.pause();
64
77
  next = false;
78
+ recorder.session.start('pause');
65
79
  store.debugMode = false;
66
80
  if (cmd === '') next = true;
67
81
  if (!cmd || cmd === 'resume' || cmd === 'exit') {
@@ -74,37 +88,78 @@ function parseInput(cmd) {
74
88
  for (const k of Object.keys(registeredVariables)) {
75
89
  eval(`var ${k} = registeredVariables['${k}'];`); // eslint-disable-line no-eval
76
90
  }
91
+
92
+ let executeCommand = Promise.resolve();
93
+
94
+ const getCmd = () => {
95
+ debug('Command:', cmd)
96
+ return cmd;
97
+ };
98
+
77
99
  store.debugMode = true;
78
100
  let isCustomCommand = false;
79
101
  let lastError = null;
102
+ let isAiCommand = false;
103
+ let $res;
80
104
  try {
81
105
  const locate = global.locate; // enable locate in this context
82
106
  const I = container.support('I');
83
107
  if (cmd.trim().startsWith('=>')) {
84
108
  isCustomCommand = true;
85
109
  cmd = cmd.trim().substring(2, cmd.length);
110
+ } else if (aiAssistant.isEnabled && !cmd.match(/^\w+\(/) && cmd.includes(' ')) {
111
+ const currentOutputLevel = output.level();
112
+ output.level(0);
113
+ const res = I.grabSource();
114
+ isAiCommand = true;
115
+ executeCommand = executeCommand.then(async () => {
116
+ try {
117
+ const html = await res;
118
+ aiAssistant.setHtmlContext(html);
119
+ } catch (err) {
120
+ output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
121
+ return;
122
+ } finally {
123
+ output.level(currentOutputLevel);
124
+ }
125
+ // aiAssistant.mockResponse("```js\nI.click('Sign in');\n```");
126
+ const spinner = ora("Processing OpenAI request...").start();
127
+ cmd = await aiAssistant.writeSteps(cmd);
128
+ spinner.stop();
129
+ output.print('');
130
+ output.print(colors.blue(aiAssistant.getResponse()));
131
+ output.print('');
132
+ return cmd;
133
+ })
86
134
  } else {
87
135
  cmd = `I.${cmd}`;
88
136
  }
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
- }
137
+ executeCommand = executeCommand.then(async () => {
138
+ const cmd = getCmd();
139
+ if (!cmd) return;
140
+ return eval(cmd); // eslint-disable-line no-eval
104
141
  }).catch((err) => {
142
+ debug(err);
143
+ if (isAiCommand) return;
105
144
  if (!lastError) output.print(output.styles.error(' ERROR '), err.message);
145
+ debug(err.stack)
146
+
106
147
  lastError = err.message;
107
- });
148
+ })
149
+
150
+ const val = await executeCommand;
151
+
152
+ if (isCustomCommand) {
153
+ if (val !== undefined) console.log('Result', '$res=', val); // eslint-disable-line
154
+ $res = val;
155
+ }
156
+
157
+ if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
158
+ output.print(output.styles.success(' OK '), cmd);
159
+ }
160
+ if (cmd?.startsWith('I.grab')) {
161
+ output.print(output.styles.debug(val));
162
+ }
108
163
 
109
164
  history.push(cmd); // add command to history when successful
110
165
  } catch (err) {
@@ -117,6 +172,7 @@ function parseInput(cmd) {
117
172
  // pop latest command from history because it failed
118
173
  history.pop();
119
174
 
175
+ if (isAiCommand) return;
120
176
  if (!lastError) output.print(output.styles.error(' FAIL '), msg);
121
177
  lastError = err.message;
122
178
  });