codeceptjs 3.5.15 → 3.6.0-beta.1.ai-healers

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 (139) hide show
  1. package/README.md +2 -2
  2. package/bin/codecept.js +66 -30
  3. package/docs/advanced.md +351 -0
  4. package/docs/ai.md +365 -0
  5. package/docs/api.md +323 -0
  6. package/docs/basics.md +979 -0
  7. package/docs/bdd.md +539 -0
  8. package/docs/best.md +237 -0
  9. package/docs/books.md +37 -0
  10. package/docs/bootstrap.md +135 -0
  11. package/docs/build/AI.js +124 -0
  12. package/docs/build/ApiDataFactory.js +410 -0
  13. package/docs/build/Appium.js +2027 -0
  14. package/docs/build/Expect.js +422 -0
  15. package/docs/build/FileSystem.js +228 -0
  16. package/docs/build/GraphQL.js +229 -0
  17. package/docs/build/GraphQLDataFactory.js +309 -0
  18. package/docs/build/JSONResponse.js +338 -0
  19. package/docs/build/Mochawesome.js +71 -0
  20. package/docs/build/Nightmare.js +2152 -0
  21. package/docs/build/Playwright.js +5110 -0
  22. package/docs/build/Protractor.js +2706 -0
  23. package/docs/build/Puppeteer.js +3905 -0
  24. package/docs/build/REST.js +344 -0
  25. package/docs/build/TestCafe.js +2125 -0
  26. package/docs/build/WebDriver.js +4240 -0
  27. package/docs/changelog.md +2572 -0
  28. package/docs/commands.md +266 -0
  29. package/docs/community-helpers.md +58 -0
  30. package/docs/configuration.md +157 -0
  31. package/docs/continuous-integration.md +22 -0
  32. package/docs/custom-helpers.md +306 -0
  33. package/docs/data.md +379 -0
  34. package/docs/detox.md +235 -0
  35. package/docs/docker.md +136 -0
  36. package/docs/email.md +183 -0
  37. package/docs/examples.md +149 -0
  38. package/docs/heal.md +186 -0
  39. package/docs/helpers/ApiDataFactory.md +266 -0
  40. package/docs/helpers/Appium.md +1374 -0
  41. package/docs/helpers/Detox.md +586 -0
  42. package/docs/helpers/Expect.md +275 -0
  43. package/docs/helpers/FileSystem.md +152 -0
  44. package/docs/helpers/GraphQL.md +151 -0
  45. package/docs/helpers/GraphQLDataFactory.md +226 -0
  46. package/docs/helpers/JSONResponse.md +254 -0
  47. package/docs/helpers/Mochawesome.md +8 -0
  48. package/docs/helpers/MockRequest.md +377 -0
  49. package/docs/helpers/Nightmare.md +1305 -0
  50. package/docs/helpers/OpenAI.md +70 -0
  51. package/docs/helpers/Playwright.md +2759 -0
  52. package/docs/helpers/Polly.md +44 -0
  53. package/docs/helpers/Protractor.md +1769 -0
  54. package/docs/helpers/Puppeteer-firefox.md +86 -0
  55. package/docs/helpers/Puppeteer.md +2317 -0
  56. package/docs/helpers/REST.md +218 -0
  57. package/docs/helpers/TestCafe.md +1321 -0
  58. package/docs/helpers/WebDriver.md +2547 -0
  59. package/docs/hooks.md +340 -0
  60. package/docs/index.md +111 -0
  61. package/docs/installation.md +75 -0
  62. package/docs/internal-api.md +266 -0
  63. package/docs/locators.md +339 -0
  64. package/docs/mobile-react-native-locators.md +67 -0
  65. package/docs/mobile.md +338 -0
  66. package/docs/pageobjects.md +291 -0
  67. package/docs/parallel.md +400 -0
  68. package/docs/playwright.md +632 -0
  69. package/docs/plugins.md +1247 -0
  70. package/docs/puppeteer.md +316 -0
  71. package/docs/quickstart.md +162 -0
  72. package/docs/react.md +70 -0
  73. package/docs/reports.md +392 -0
  74. package/docs/secrets.md +36 -0
  75. package/docs/shadow.md +68 -0
  76. package/docs/shared/keys.mustache +31 -0
  77. package/docs/shared/react.mustache +1 -0
  78. package/docs/testcafe.md +174 -0
  79. package/docs/translation.md +247 -0
  80. package/docs/tutorial.md +271 -0
  81. package/docs/typescript.md +180 -0
  82. package/docs/ui.md +59 -0
  83. package/docs/videos.md +28 -0
  84. package/docs/visual.md +202 -0
  85. package/docs/vue.md +143 -0
  86. package/docs/webdriver.md +701 -0
  87. package/docs/wiki/Books-&-Posts.md +27 -0
  88. package/docs/wiki/Community-Helpers-&-Plugins.md +53 -0
  89. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +61 -0
  90. package/docs/wiki/Examples.md +145 -0
  91. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +68 -0
  92. package/docs/wiki/Home.md +16 -0
  93. package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
  94. package/docs/wiki/Release-Process.md +24 -0
  95. package/docs/wiki/Roadmap.md +23 -0
  96. package/docs/wiki/Tests.md +1393 -0
  97. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +153 -0
  98. package/docs/wiki/Videos.md +19 -0
  99. package/lib/actor.js +3 -6
  100. package/lib/ai.js +152 -80
  101. package/lib/cli.js +1 -0
  102. package/lib/command/dryRun.js +13 -44
  103. package/lib/command/generate.js +34 -0
  104. package/lib/command/run-workers.js +3 -0
  105. package/lib/command/run.js +3 -0
  106. package/lib/container.js +2 -0
  107. package/lib/heal.js +172 -0
  108. package/lib/helper/AI.js +124 -0
  109. package/lib/helper/Appium.js +12 -36
  110. package/lib/helper/Expect.js +8 -11
  111. package/lib/helper/JSONResponse.js +8 -8
  112. package/lib/helper/Playwright.js +240 -100
  113. package/lib/helper/Puppeteer.js +68 -182
  114. package/lib/helper/REST.js +1 -4
  115. package/lib/helper/WebDriver.js +10 -324
  116. package/lib/index.js +3 -0
  117. package/lib/listener/steps.js +0 -2
  118. package/lib/locator.js +4 -13
  119. package/lib/plugin/coverage.js +99 -112
  120. package/lib/plugin/heal.js +26 -117
  121. package/lib/recorder.js +11 -5
  122. package/lib/step.js +1 -3
  123. package/lib/store.js +2 -0
  124. package/lib/template/heal.js +39 -0
  125. package/package.json +35 -47
  126. package/typings/index.d.ts +0 -17
  127. package/typings/promiseBasedTypes.d.ts +57 -340
  128. package/typings/types.d.ts +73 -433
  129. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  130. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  131. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  132. package/docs/webapi/seeTraffic.mustache +0 -36
  133. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  134. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  135. package/docs/webapi/waitForCookie.mustache +0 -9
  136. package/lib/helper/MockServer.js +0 -221
  137. package/lib/helper/errors/ElementAssertion.js +0 -38
  138. package/lib/helper/networkTraffics/utils.js +0 -137
  139. /package/{lib/helper → docs/build}/OpenAI.js +0 -0
package/lib/heal.js ADDED
@@ -0,0 +1,172 @@
1
+ const debug = require('debug')('codeceptjs:heal');
2
+ const colors = require('chalk');
3
+ const Container = require('./container');
4
+ const recorder = require('./recorder');
5
+ const output = require('./output');
6
+ const event = require('./event');
7
+
8
+ /**
9
+ * @class
10
+ */
11
+ class Heal {
12
+ constructor() {
13
+ this.recipes = {};
14
+ this.fixes = [];
15
+ this.prepareFns = [];
16
+ this.contextName = null;
17
+ this.numHealed = 0;
18
+ }
19
+
20
+ clear() {
21
+ this.recipes = {};
22
+ this.fixes = [];
23
+ this.prepareFns = [];
24
+ this.contextName = null;
25
+ this.numHealed = 0;
26
+ }
27
+
28
+ addRecipe(name, opts = {}) {
29
+ if (!opts.priority) opts.priority = 0;
30
+
31
+ if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`);
32
+
33
+ this.recipes[name] = opts;
34
+ }
35
+
36
+ connectToEvents() {
37
+ event.dispatcher.on(event.suite.before, (suite) => {
38
+ this.contextName = suite.title;
39
+ });
40
+
41
+ event.dispatcher.on(event.test.started, (test) => {
42
+ this.contextName = test.fullTitle();
43
+ });
44
+
45
+ event.dispatcher.on(event.test.finished, () => {
46
+ this.contextName = null;
47
+ });
48
+ }
49
+
50
+ hasCorrespondingRecipes(step) {
51
+ return matchRecipes(this.recipes, this.contextName)
52
+ .filter(r => !r.steps || r.steps.includes(step.name))
53
+ .length > 0;
54
+ }
55
+
56
+ async getCodeSuggestions(context) {
57
+ const suggestions = [];
58
+ const recipes = matchRecipes(this.recipes, this.contextName);
59
+
60
+ debug('Recipes', recipes);
61
+
62
+ const currentOutputLevel = output.level();
63
+ output.level(0);
64
+
65
+ for (const [property, prepareFn] of Object.entries(recipes.map(r => r.prepare).filter(p => !!p).reduce((acc, obj) => ({ ...acc, ...obj }), {}))) {
66
+ if (!prepareFn) continue;
67
+
68
+ if (context[property]) continue;
69
+ context[property] = await prepareFn(Container.support());
70
+ }
71
+
72
+ output.level(currentOutputLevel);
73
+
74
+ for (const recipe of recipes) {
75
+ let snippets = await recipe.fn(context);
76
+ if (!Array.isArray(snippets)) snippets = [snippets];
77
+
78
+ suggestions.push({
79
+ name: recipe.name,
80
+ snippets,
81
+ });
82
+ }
83
+
84
+ return suggestions.filter(s => !isBlank(s.snippets));
85
+ }
86
+
87
+ async healStep(failedStep, error, failureContext = {}) {
88
+ output.debug(`Trying to heal ${failedStep.toCode()} step`);
89
+
90
+ Object.assign(failureContext, {
91
+ error,
92
+ step: failedStep,
93
+ prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
94
+ });
95
+
96
+ const suggestions = await this.getCodeSuggestions(failureContext);
97
+
98
+ if (suggestions.length === 0) {
99
+ debug('No healing suggestions found');
100
+ throw error;
101
+ }
102
+
103
+ output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
104
+
105
+ debug(suggestions);
106
+
107
+ for (const suggestion of suggestions) {
108
+ for (const codeSnippet of suggestion.snippets) {
109
+ try {
110
+ debug('Executing', codeSnippet);
111
+ recorder.catch((e) => {
112
+ debug(e);
113
+ });
114
+
115
+ if (typeof codeSnippet === 'string') {
116
+ const I = Container.support('I'); // eslint-disable-line
117
+ await eval(codeSnippet); // eslint-disable-line
118
+ } else if (typeof codeSnippet === 'function') {
119
+ await codeSnippet(Container.support());
120
+ }
121
+
122
+ this.fixes.push({
123
+ recipe: suggestion.name,
124
+ test: failureContext?.test,
125
+ step: failedStep,
126
+ snippet: codeSnippet,
127
+ });
128
+
129
+ recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')));
130
+ this.numHealed++;
131
+ // recorder.session.restore();
132
+ return;
133
+ } catch (err) {
134
+ debug('Failed to execute code', err);
135
+ recorder.ignoreErr(err); // healing did not help
136
+ recorder.catchWithoutStop(err);
137
+ await recorder.promise(); // wait for all promises to resolve
138
+ }
139
+ }
140
+ }
141
+ output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
142
+ recorder.throw(error);
143
+ }
144
+
145
+ static setDefaultHealers() {
146
+ require('./template/heal');
147
+ }
148
+ }
149
+
150
+ const heal = new Heal();
151
+
152
+ module.exports = heal;
153
+
154
+ function matchRecipes(recipes, contextName) {
155
+ return Object.entries(recipes)
156
+ .filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
157
+ .sort(([, a], [, b]) => b.priority - a.priority)
158
+ .map(([name, recipe]) => {
159
+ recipe.name = name;
160
+ return recipe;
161
+ })
162
+ .filter(r => !!r.fn);
163
+ }
164
+
165
+ function isBlank(value) {
166
+ return (
167
+ value == null
168
+ || (Array.isArray(value) && value.length === 0)
169
+ || (typeof value === 'object' && Object.keys(value).length === 0)
170
+ || (typeof value === 'string' && value.trim() === '')
171
+ );
172
+ }
@@ -0,0 +1,124 @@
1
+ const Helper = require('@codeceptjs/helper');
2
+ const ai = require('../ai');
3
+ const standardActingHelpers = require('../plugin/standardActingHelpers');
4
+ const Container = require('../container');
5
+ const { splitByChunks, minifyHtml } = require('../html');
6
+
7
+ /**
8
+ * AI Helper for CodeceptJS.
9
+ *
10
+ * This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
11
+ * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available.
12
+ *
13
+ * ## Configuration
14
+ *
15
+ * This helper should be configured in codecept.json or codecept.conf.js
16
+ *
17
+ * * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
18
+ */
19
+ class AI extends Helper {
20
+ constructor(config) {
21
+ super(config);
22
+ this.aiAssistant = ai;
23
+
24
+ this.options = {
25
+ chunkSize: 80000,
26
+ };
27
+ this.options = { ...this.options, ...config };
28
+ }
29
+
30
+ _beforeSuite() {
31
+ const helpers = Container.helpers();
32
+
33
+ for (const helperName of standardActingHelpers) {
34
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
35
+ this.helper = helpers[helperName];
36
+ break;
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Asks the AI GPT language model a question based on the provided prompt within the context of the current page's HTML.
43
+ *
44
+ * ```js
45
+ * I.askGptOnPage('what does this page do?');
46
+ * ```
47
+ *
48
+ * @async
49
+ * @param {string} prompt - The question or prompt to ask the GPT model.
50
+ * @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
51
+ */
52
+ async askGptOnPage(prompt) {
53
+ const html = await this.helper.grabSource();
54
+
55
+ const htmlChunks = splitByChunks(html, this.options.chunkSize);
56
+
57
+ if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`);
58
+
59
+ const responses = [];
60
+
61
+ for (const chunk of htmlChunks) {
62
+ const messages = [
63
+ { role: 'user', content: prompt },
64
+ { role: 'user', content: `Within this HTML: ${minifyHtml(chunk)}` },
65
+ ];
66
+
67
+ if (htmlChunks.length > 1) messages.push({ role: 'user', content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment' });
68
+
69
+ const response = await this.aiAssistant.createCompletion(messages);
70
+
71
+ console.log(response);
72
+
73
+ responses.push(response);
74
+ }
75
+
76
+ return responses.join('\n\n');
77
+ }
78
+
79
+ /**
80
+ * Asks the AI a question based on the provided prompt within the context of a specific HTML fragment on the current page.
81
+ *
82
+ * ```js
83
+ * I.askGptOnPageFragment('describe features of this screen', '.screen');
84
+ * ```
85
+ *
86
+ * @async
87
+ * @param {string} prompt - The question or prompt to ask the GPT-3.5 model.
88
+ * @param {string} locator - The locator or selector used to identify the HTML fragment on the page.
89
+ * @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
90
+ */
91
+ async askGptOnPageFragment(prompt, locator) {
92
+ const html = await this.helper.grabHTMLFrom(locator);
93
+
94
+ const messages = [
95
+ { role: 'user', content: prompt },
96
+ { role: 'user', content: `Within this HTML: ${minifyHtml(html)}` },
97
+ ];
98
+
99
+ const response = await this.aiAssistant.createCompletion(messages);
100
+
101
+ console.log(response);
102
+
103
+ return response;
104
+ }
105
+
106
+ /**
107
+ * Send a general request to ChatGPT and return response.
108
+ * @param {string} prompt
109
+ * @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
110
+ */
111
+ async askGptGeneralPrompt(prompt) {
112
+ const messages = [
113
+ { role: 'user', content: prompt },
114
+ ];
115
+
116
+ const response = await this.aiAssistant.createCompletion(messages);
117
+
118
+ console.log(response);
119
+
120
+ return response;
121
+ }
122
+ }
123
+
124
+ module.exports = AI;
@@ -2,7 +2,6 @@ let webdriverio;
2
2
 
3
3
  const fs = require('fs');
4
4
  const axios = require('axios').default;
5
- const { v4: uuidv4 } = require('uuid');
6
5
 
7
6
  const Webdriver = require('./WebDriver');
8
7
  const AssertionFailedError = require('../assert/error');
@@ -1089,40 +1088,17 @@ class Appium extends Webdriver {
1089
1088
  * Appium: support Android and iOS
1090
1089
  */
1091
1090
  async performSwipe(from, to) {
1092
- await this.browser.performActions([{
1093
- id: uuidv4(),
1094
- type: 'pointer',
1095
- parameters: {
1096
- pointerType: 'touch',
1097
- },
1098
- actions: [
1099
- {
1100
- duration: 0,
1101
- x: from.x,
1102
- y: from.y,
1103
- type: 'pointerMove',
1104
- origin: 'viewport',
1105
- },
1106
- {
1107
- button: 1,
1108
- type: 'pointerDown',
1109
- },
1110
- {
1111
- duration: 200,
1112
- type: 'pause',
1113
- },
1114
- {
1115
- duration: 600,
1116
- x: to.x,
1117
- y: to.y,
1118
- type: 'pointerMove',
1119
- origin: 'viewport',
1120
- },
1121
- {
1122
- button: 1,
1123
- type: 'pointerUp',
1124
- },
1125
- ],
1091
+ await this.browser.touchPerform([{
1092
+ action: 'press',
1093
+ options: from,
1094
+ }, {
1095
+ action: 'wait',
1096
+ options: { ms: 1000 },
1097
+ }, {
1098
+ action: 'moveTo',
1099
+ options: to,
1100
+ }, {
1101
+ action: 'release',
1126
1102
  }]);
1127
1103
  await this.browser.pause(1000);
1128
1104
  }
@@ -1152,7 +1128,7 @@ class Appium extends Webdriver {
1152
1128
  yoffset = 100;
1153
1129
  }
1154
1130
 
1155
- return this.swipe(parseLocator.call(this, locator), 0, yoffset, speed);
1131
+ return this.swipe(locator, 0, yoffset, speed);
1156
1132
  }
1157
1133
 
1158
1134
  /**
@@ -1,15 +1,12 @@
1
+ const chai = require('chai');
1
2
  const output = require('../output');
2
3
 
3
- let expect;
4
+ const { expect } = chai;
4
5
 
5
- import('chai').then(chai => {
6
- expect = chai.expect;
7
- chai.use(require('chai-string'));
8
- // @ts-ignore
9
- chai.use(require('chai-exclude'));
10
- chai.use(require('chai-match-pattern'));
11
- chai.use(require('chai-json-schema'));
12
- });
6
+ chai.use(require('chai-string'));
7
+ // @ts-ignore
8
+ chai.use(require('chai-exclude'));
9
+ chai.use(require('chai-match-pattern'));
13
10
 
14
11
  /**
15
12
  * This helper allows performing assertions based on Chai.
@@ -182,7 +179,7 @@ class ExpectHelper {
182
179
  expectJsonSchema(targetData, jsonSchema, customErrorMsg = '') {
183
180
  // @ts-ignore
184
181
  output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`);
185
-
182
+ chai.use(require('chai-json-schema'));
186
183
  return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema);
187
184
  }
188
185
 
@@ -190,7 +187,7 @@ class ExpectHelper {
190
187
  * @param {*} targetData
191
188
  * @param {*} jsonSchema
192
189
  * @param {*} [customErrorMsg]
193
- * @param {*} [ajvOptions] Pass AJV options
190
+ * @param {*} ajvOptions Pass AJV options
194
191
  */
195
192
  expectJsonSchemaUsingAJV(
196
193
  targetData,
@@ -1,13 +1,12 @@
1
+ const assert = require('assert');
2
+ const chai = require('chai');
3
+ const joi = require('joi');
4
+ const chaiDeepMatch = require('chai-deep-match');
1
5
  const Helper = require('@codeceptjs/helper');
2
6
 
3
- let expect;
4
-
5
- import('chai').then(chai => {
6
- expect = chai.expect;
7
- chai.use(require('chai-deep-match'));
8
- });
7
+ chai.use(chaiDeepMatch);
9
8
 
10
- const joi = require('joi');
9
+ const { expect } = chai;
11
10
 
12
11
  /**
13
12
  * This helper allows performing assertions on JSON responses paired with following helpers:
@@ -92,6 +91,7 @@ class JSONResponse extends Helper {
92
91
  static _checkRequirements() {
93
92
  try {
94
93
  require('joi');
94
+ require('chai');
95
95
  } catch (e) {
96
96
  return ['joi'];
97
97
  }
@@ -194,7 +194,7 @@ class JSONResponse extends Helper {
194
194
  fails++;
195
195
  }
196
196
  }
197
- expect(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`).to.be.true;
197
+ assert.ok(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`);
198
198
  } else {
199
199
  expect(this.response.data).to.deep.match(json);
200
200
  }