codeceptjs 3.5.14 → 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 (137) 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/generate.js +34 -0
  103. package/lib/command/run-workers.js +3 -0
  104. package/lib/command/run.js +3 -0
  105. package/lib/container.js +2 -0
  106. package/lib/heal.js +172 -0
  107. package/lib/helper/AI.js +124 -0
  108. package/lib/helper/Appium.js +12 -36
  109. package/lib/helper/Expect.js +7 -10
  110. package/lib/helper/JSONResponse.js +8 -8
  111. package/lib/helper/Playwright.js +240 -100
  112. package/lib/helper/Puppeteer.js +9 -61
  113. package/lib/helper/REST.js +1 -4
  114. package/lib/helper/WebDriver.js +10 -324
  115. package/lib/index.js +3 -0
  116. package/lib/listener/steps.js +0 -2
  117. package/lib/locator.js +4 -13
  118. package/lib/plugin/heal.js +26 -117
  119. package/lib/recorder.js +11 -5
  120. package/lib/step.js +1 -3
  121. package/lib/store.js +2 -0
  122. package/lib/template/heal.js +39 -0
  123. package/package.json +23 -27
  124. package/typings/index.d.ts +0 -16
  125. package/typings/promiseBasedTypes.d.ts +55 -338
  126. package/typings/types.d.ts +58 -353
  127. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  128. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  129. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  130. package/docs/webapi/seeTraffic.mustache +0 -36
  131. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  132. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  133. package/docs/webapi/waitForCookie.mustache +0 -9
  134. package/lib/helper/MockServer.js +0 -221
  135. package/lib/helper/errors/ElementAssertion.js +0 -38
  136. package/lib/helper/networkTraffics/utils.js +0 -137
  137. /package/{lib/helper → docs/build}/OpenAI.js +0 -0
@@ -0,0 +1,153 @@
1
+ 🚀 CodeceptJS 3 is out now. Install it:
2
+
3
+ ```
4
+ npm i codeceptjs
5
+ ```
6
+
7
+ * [COMPLETE CHANGELOG](https://github.com/Codeception/CodeceptJS/blob/codeceptjs-v3.0/CHANGELOG.md#300-beta)
8
+ * [Documentation](https://github.com/Codeception/CodeceptJS/tree/codeceptjs-v3.0/docs)
9
+
10
+ After installing a new version, run `codecept3-upgrade` which is described below:
11
+
12
+ ---
13
+
14
+ CodeceptJS 3.0 is a new version of CodeceptJS with some breaking changes included.
15
+ You should update your project following this guide to ensure that everything works correctly.
16
+
17
+ ### 1️⃣ Syntax Change
18
+
19
+ CodeceptJS 3.0 introduced a new syntax for declaring tests, instead of:
20
+
21
+ ```js
22
+ Scenario('title', (I, loginPage) => {})
23
+ ```
24
+ we use a new way of passing arguments into a test, via destruction:
25
+
26
+ ```js
27
+ Scenario('title', ({ I, loginPage }) => {})
28
+ ```
29
+
30
+ This works similarly to `inject()` command. This change was done to unify accessing [dependency injection container](https://codecept.io/pageobjects/#dependency-injection), and to provide better TypeScript support.
31
+
32
+ > If you use BDD-style project with Gherkin, no changes needed for this step.
33
+
34
+ To upgrade your project, you don't need to change manually all your tests.
35
+
36
+ 💪 **Use [codecept3-upgrade tool](https://www.npmjs.com/package/codecept3-upgrade) to perform the migration**. Use it carefully, as it updates your current code.
37
+
38
+ To upgrade your codebase, commit all your changes, and switch to a new branch.
39
+ Run upgrade script (even without installing) for a directory where you have your tests:
40
+
41
+ ```
42
+ npx codecept3-upgrade tests
43
+ ```
44
+ Review the changes in Git Diff and try to execute tests using CodeceptJS 3.0
45
+
46
+
47
+ ### 2️⃣ Grabbers signature change
48
+
49
+ There were confusion and inconsistency across grab* methods. What they will return if multiple elements are found? A single element or an array? This was the problem as the format was dependent on a page content.
50
+
51
+ In 3.0 we decided to make all current grab* methods to return a single value only. While we add new methods grab**FromAll that return an array.
52
+
53
+ For instance, `grabTextFrom` will always return a single text value, while `grabTextFromAll` will return an array of values:
54
+
55
+ ```js
56
+ await I.grabTextFrom('.username'); => 'john'
57
+ await I.grabTextFromAll('.username'); => ['john', 'claudia', 'bill']
58
+ ```
59
+
60
+ Please update parts in your project where you rely on grab* methods to return an array.
61
+
62
+
63
+ | Single Value | Multiple Values |
64
+ | -- | -- |
65
+ | ✋`grabTextFrom` | 🙌 `grabTextFromAll` |
66
+ | ✋`grabValueFrom` | 🙌 `grabValueFromAll` |
67
+ | ✋ `grabAttributeFrom` | 🙌`grabAttributeFromAll` |
68
+ | ✋ `grabHTMLFrom` | 🙌 `grabHTMLFromAll` |
69
+ | ✋ `grabCssPropertyFrom` | 🙌 `grabCssPropertyFromAll` |
70
+
71
+ > Single-value `grab*From` will throw error when no data was matched, while `grab*FromAll` will return array.
72
+
73
+ ### 3️⃣ Bootstrap / Teardown Changed
74
+
75
+ `async/await` paradigm changed the way we write asynchronous code in NodeJS.
76
+ However, bootstrap functions were created to use old-style methods of passing `done` callback inside.
77
+
78
+ In 3.0 we decided to completely change the way async bootstrap is performed and replace all it with async/await functions:
79
+
80
+ ```js
81
+ // before
82
+ bootstrap: (done) => {
83
+ server.start().then(done);
84
+ },
85
+
86
+ // after
87
+ bootstrap: async () => {
88
+ await server.start();
89
+ }
90
+ ```
91
+ Passing a string as bootstrap function (to require a function from external file) is also not supported:
92
+
93
+ ```js
94
+ // before
95
+ bootstrap: './server_start.js',
96
+
97
+ // after
98
+ bootstrap: require('./server_start.js'),
99
+ ```
100
+
101
+ The same rules are applied to teardown, bootstrapAll, teardownAll.
102
+
103
+ ### 4️⃣ Locator Detection Heuristic Change
104
+
105
+ In 3.0 we added a new rule to auto-detect CSS locator. If a locator starts with `[` parser will use it as a CSS locator, without trying to match value by text.
106
+
107
+ * Previous behavior: I.click('[user]') - will try to search for a link with `[user]` text, if no found - take `[user]` as CSS locator
108
+ * Current behavior: I.click('[user]') - will check only for CSS locator `[user]`
109
+
110
+ ### 5️⃣ Improved Parallel Execution with Workers
111
+
112
+ CodeceptJS has two parallel execution modes:
113
+
114
+ * classical `run-multiple` which spawns independent CodeceptJS processes on system level.
115
+ * threaded `run-workers` which uses NodeJS workers instead of subprocesses.
116
+
117
+ Workers are faster, they can communicate with parent thread. Unfortunately, `run-workers` was not as efficient as `run-multiple` because there was no way of declaring sophisticated configs for parallel execution. For instance, running tests in 4 threads in 2 browsers could not be set.
118
+
119
+ We updated workers API to make them as flexible as possible. You don't even need to put complex logic into config! You can create your own parallel runner and customize it to match your expectations:
120
+
121
+ ```js
122
+ // don't initialize workers in constructor
123
+ const workers = new Workers(null, workerConfig);
124
+ // split tests by suites in 2 groups
125
+ const testGroups = workers.createGroupsOfSuites(2);
126
+
127
+ const browsers = ['firefox', 'chrome'];
128
+
129
+ const configs = browsers.map(browser => {
130
+ return helpers: {
131
+ WebDriver: { browser }
132
+ }
133
+ });
134
+
135
+ for (const config of configs) {
136
+ for (group of groupOfTests) {
137
+ const worker = workers.spawn();
138
+ worker.addTests(group);
139
+ worker.addConfig(config);
140
+ }
141
+ }
142
+
143
+ workers.run();
144
+ ```
145
+ [Learn more about how you can create a parallel runner](https://github.com/Codeception/CodeceptJS/blob/codeceptjs-v3.0/docs/parallel.md#custom-parallel-execution).
146
+
147
+ `run-multiple` will still work but it is considered deprecated and won't be supported.
148
+
149
+ ### WebDriverIO helper removed
150
+
151
+ WebDriverIO helper supported webdriverio v4 library, which is not maintained anymore. It should be easy to switch to WebDriver helper which supports webdriverio v6.
152
+
153
+
@@ -0,0 +1,19 @@
1
+ [![](http://i3.ytimg.com/vi/BRMWstiOTks/maxresdefault.jpg)](https://www.youtube.com/watch?v=BRMWstiOTks)
2
+
3
+ ## [An Introduction, Getting started and working with CodeceptJS & Puppeteer (EAWeekend)](https://www.youtube.com/watch?v=BRMWstiOTks)
4
+
5
+ ## [CodeceptJS Official YouTube Channel](https://www.youtube.com/channel/UCEs4030bmtonyDhTHEXa_2g)
6
+
7
+ ## [Introductory Videos](https://www.youtube.com/watch?v=FPFG1rBNJ64&list=PLcFXthgti9Lt4SjSvL1ALDg6dOeTC0TvT)
8
+
9
+ Free educational videos provided by our community member **[@ontytoom](http://github.com/ontytoom)**.
10
+
11
+ 1. [Installation](https://www.youtube.com/watch?v=FPFG1rBNJ64)
12
+ 1. [Creating a Test](https://www.youtube.com/watch?v=mdQZjL3h9d0)
13
+ 1. [Using Page Objects](https://www.youtube.com/watch?v=s677_6VctjQ)
14
+
15
+ ## [Practical E2E Testing with CodeceptJS](https://www.udemy.com/practical-e2e-testing-with-codeceptjs/)
16
+
17
+ Udemy course by Luke Beilharz
18
+
19
+
package/lib/actor.js CHANGED
@@ -13,18 +13,15 @@ const output = require('./output');
13
13
  */
14
14
  class Actor {
15
15
  /**
16
- * Print the comment on log. Also, adding a step in the `Test.steps` object
16
+ * add print comment method`
17
17
  * @param {string} msg
18
18
  * @param {string} color
19
19
  * @inner
20
20
  *
21
21
  * ⚠️ returns a promise which is synchronized internally by recorder
22
22
  */
23
- async say(msg, color = 'cyan') {
24
- const step = new Step('say', 'say');
25
- step.status = 'passed';
26
- return recordStep(step, [msg]).then(() => {
27
- // this is backward compatibility as this event may be used somewhere
23
+ say(msg, color = 'cyan') {
24
+ return recorder.add(`say ${msg}`, () => {
28
25
  event.emit(event.step.comment, msg);
29
26
  output.say(msg, `${color}`);
30
27
  });
package/lib/ai.js CHANGED
@@ -1,47 +1,118 @@
1
- const { Configuration, OpenAIApi } = require('openai');
2
1
  const debug = require('debug')('codeceptjs:ai');
3
- const config = require('./config');
4
2
  const output = require('./output');
3
+ const event = require('./event');
5
4
  const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html');
6
5
 
7
- const defaultConfig = {
8
- model: 'gpt-3.5-turbo-16k',
9
- temperature: 0.1,
10
- };
11
-
12
- const htmlConfig = {
6
+ const defaultHtmlConfig = {
13
7
  maxLength: 50000,
14
8
  simplify: true,
15
9
  minify: true,
16
10
  html: {},
17
11
  };
18
12
 
19
- const aiInstance = null;
13
+ const defaultPrompts = {
14
+ writeStep: (html, input) => [{
15
+ role: 'user',
16
+ content: `I am test engineer writing test in CodeceptJS
17
+ I have opened web page and I want to use CodeceptJS to ${input} on this page
18
+ Provide me valid CodeceptJS code to accomplish it
19
+ Use only locators from this HTML: \n\n${html}`,
20
+ },
21
+ ],
22
+
23
+ healStep: (html, { step, error, prevSteps }) => {
24
+ return [{
25
+ role: 'user',
26
+ content: `As a test automation engineer I am testing web application using CodeceptJS.
27
+ I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
28
+ Propose how to adjust ${step.toCode()} step to fix the test.
29
+ Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
30
+ Here is the error message: ${error.message}
31
+ Here is HTML code of a page where the failure has happened: \n\n${html}`,
32
+ }];
33
+ },
34
+ };
20
35
 
21
36
  class AiAssistant {
22
37
  constructor() {
23
- this.config = config.get('ai', defaultConfig);
24
- this.htmlConfig = Object.assign(htmlConfig, this.config.html);
25
- delete this.config.html;
26
- this.html = null;
38
+ this.totalTime = 0;
39
+ this.numTokens = 0;
40
+
41
+ this.reset();
42
+ this.connectToEvents();
43
+ }
44
+
45
+ enable(config = {}) {
46
+ debug('Enabling AI assistant');
47
+ this.isEnabled = true;
48
+
49
+ const { html, prompts, ...aiConfig } = config;
50
+
51
+ this.config = Object.assign(this.config, aiConfig);
52
+ this.htmlConfig = Object.assign(defaultHtmlConfig, html);
53
+ this.prompts = Object.assign(defaultPrompts, prompts);
54
+
55
+ debug('Config', this.config);
56
+ }
57
+
58
+ reset() {
59
+ this.numTokens = 0;
60
+ this.isEnabled = false;
61
+ this.config = {
62
+ maxTokens: 1000000,
63
+ request: null,
64
+ response: parseCodeBlocks,
65
+ // lets limit token usage to 1M
66
+ };
67
+ this.minifiedHtml = null;
27
68
  this.response = null;
69
+ this.totalTime = 0;
70
+ }
71
+
72
+ disable() {
73
+ this.isEnabled = false;
74
+ }
28
75
 
29
- this.isEnabled = !!process.env.OPENAI_API_KEY;
76
+ connectToEvents() {
77
+ event.dispatcher.on(event.all.result, () => {
78
+ if (this.isEnabled && this.numTokens > 0) {
79
+ const numTokensK = Math.ceil(this.numTokens / 1000);
80
+ const maxTokensK = Math.ceil(this.config.maxTokens / 1000);
81
+ output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`);
82
+ }
83
+ });
84
+ }
30
85
 
86
+ checkRequestFn() {
31
87
  if (!this.isEnabled) {
32
- debug('No OpenAI API key provided. AI assistant is disabled.');
88
+ debug('AI assistant is disabled');
33
89
  return;
34
90
  }
35
91
 
36
- const configuration = new Configuration({
37
- apiKey: process.env.OPENAI_API_KEY,
38
- });
92
+ if (this.config.request) return;
39
93
 
40
- this.openai = new OpenAIApi(configuration);
41
- }
94
+ const noRequestErrorMessage = `
95
+ No request function is set for AI assistant.
96
+ Please implement your own request function and set it in the config.
97
+
98
+ [!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service, please implement your own request function and set it in the config.
99
+
100
+ Example (connect to OpenAI):
101
+
102
+ ai: {
103
+ request: async (messages) => {
104
+ const OpenAI = require('openai');
105
+ const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] })
106
+ const response = await openai.chat.completions.create({
107
+ model: 'gpt-3.5-turbo-0125',
108
+ messages,
109
+ });
110
+ return response?.data?.choices[0]?.message?.content;
111
+ }
112
+ }
113
+ `.trim();
42
114
 
43
- static getInstance() {
44
- return aiInstance || new AiAssistant();
115
+ throw new Error(noRequestErrorMessage);
45
116
  }
46
117
 
47
118
  async setHtmlContext(html) {
@@ -50,98 +121,99 @@ class AiAssistant {
50
121
  if (this.htmlConfig.simplify) {
51
122
  processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
52
123
  }
124
+
53
125
  if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
54
126
  if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
55
127
 
56
- debug(processedHTML);
57
-
58
- this.html = processedHTML;
128
+ this.minifiedHtml = processedHTML;
59
129
  }
60
130
 
61
131
  getResponse() {
62
132
  return this.response || '';
63
133
  }
64
134
 
65
- mockResponse(response) {
66
- this.mockedResponse = response;
67
- }
68
-
69
135
  async createCompletion(messages) {
70
- if (!this.openai) return;
136
+ if (!this.isEnabled) return '';
71
137
 
72
- debug(messages);
138
+ debug('Request', messages);
73
139
 
74
- if (this.mockedResponse) return this.mockedResponse;
140
+ this.checkRequestFn();
75
141
 
76
142
  this.response = null;
77
143
 
78
- try {
79
- const completion = await this.openai.createChatCompletion({
80
- ...this.config,
81
- messages,
82
- });
83
-
84
- this.response = completion?.data?.choices[0]?.message?.content;
85
-
86
- debug(this.response);
144
+ this.calculateTokens(messages);
87
145
 
146
+ try {
147
+ const startTime = process.hrtime();
148
+ this.response = await this.config.request(messages);
149
+ const endTime = process.hrtime(startTime);
150
+ const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9;
151
+
152
+ this.totalTime += Math.round(executionTimeInSeconds);
153
+ debug('AI response time', executionTimeInSeconds);
154
+ debug('Response', this.response);
155
+ this.stopWhenReachingTokensLimit();
88
156
  return this.response;
89
157
  } catch (err) {
90
158
  debug(err.response);
91
159
  output.print('');
92
- output.error(`OpenAI error: ${err.message}`);
93
- output.error(err?.response?.data?.error?.code);
94
- output.error(err?.response?.data?.error?.message);
160
+ output.error(`AI service error: ${err.message}`);
161
+ if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code);
162
+ if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message);
163
+ this.stopWhenReachingTokensLimit();
95
164
  return '';
96
165
  }
97
166
  }
98
167
 
99
- async healFailedStep(step, err, test) {
168
+ async healFailedStep(failureContext) {
100
169
  if (!this.isEnabled) return [];
101
- if (!this.html) throw new Error('No HTML context provided');
102
-
103
- const messages = [
104
- { role: 'user', content: 'As a test automation engineer I am testing web application using CodeceptJS.' },
105
- { role: 'user', content: `I want to heal a test that fails. Here is the list of executed steps: ${test.steps.join(', ')}` },
106
- { role: 'user', content: `Propose how to adjust ${step.toCode()} step to fix the test.` },
107
- { role: 'user', content: 'Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with ```.' },
108
- { role: 'user', content: `Here is the error message: ${err.message}` },
109
- { role: 'user', content: `Here is HTML code of a page where the failure has happened: \n\n${this.html}` },
110
- ];
111
-
112
- const response = await this.createCompletion(messages);
170
+ if (!failureContext.html) throw new Error('No HTML context provided');
171
+
172
+ await this.setHtmlContext(failureContext.html);
173
+
174
+ if (!this.minifiedHtml) {
175
+ debug('HTML context is empty after removing non-interactive elements & minification');
176
+ return [];
177
+ }
178
+
179
+ const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext));
113
180
  if (!response) return [];
114
181
 
115
- return parseCodeBlocks(response);
182
+ return this.config.response(response);
183
+ }
184
+
185
+ calculateTokens(messages) {
186
+ // we implement naive approach for calculating tokens with no extra requests
187
+ // this approach was tested via https://platform.openai.com/tokenizer
188
+ // we need it to display current usage tokens usage so users could analyze effectiveness of AI
189
+
190
+ const inputString = messages.map(m => m.content).join(' ').trim();
191
+ const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length;
192
+
193
+ // 2.5 token is constant for average HTML input
194
+ const tokens = numWords * 2.5;
195
+
196
+ this.numTokens += tokens;
197
+
198
+ return tokens;
199
+ }
200
+
201
+ stopWhenReachingTokensLimit() {
202
+ if (this.numTokens < this.config.maxTokens) return;
203
+
204
+ output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`);
205
+ this.disable();
116
206
  }
117
207
 
118
208
  async writeSteps(input) {
119
209
  if (!this.isEnabled) return;
120
- if (!this.html) throw new Error('No HTML context provided');
210
+ if (!this.minifiedHtml) throw new Error('No HTML context provided');
121
211
 
122
212
  const snippets = [];
123
213
 
124
- const messages = [
125
- {
126
- role: 'user',
127
- content: `I am test engineer writing test in CodeceptJS
128
- I have opened web page and I want to use CodeceptJS to ${input} on this page
129
- Provide me valid CodeceptJS code to accomplish it
130
- Use only locators from this HTML: \n\n${this.html}`,
131
- },
132
- { role: 'user', content: 'Propose only CodeceptJS steps code. Do not include Scenario or Feature into response' },
133
-
134
- // old prompt
135
- // { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Submit</button></body></html>' },
136
- // { role: 'assistant', content: '```js\nI.click("Submit");\n```' },
137
- // { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Login</button></body></html>' },
138
- // { role: 'assistant', content: 'No suggestions' },
139
- // { role: 'user', content: `Now I want to ${input} on this HTML page using CodeceptJS code` },
140
- // { role: 'user', content: `Provide me with CodeceptJS code to achieve this on THIS page.` },
141
- ];
142
- const response = await this.createCompletion(messages);
214
+ const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input));
143
215
  if (!response) return;
144
- snippets.push(...parseCodeBlocks(response));
216
+ snippets.push(...this.config.response(response));
145
217
 
146
218
  debug(snippets[0]);
147
219
 
@@ -177,4 +249,4 @@ function parseCodeBlocks(response) {
177
249
  return modifiedSnippets.filter(snippet => !!snippet);
178
250
  }
179
251
 
180
- module.exports = AiAssistant;
252
+ module.exports = new AiAssistant();
package/lib/cli.js CHANGED
@@ -167,6 +167,7 @@ class Cli extends Base {
167
167
  }
168
168
 
169
169
  const steps = test.steps || (test.ctx && test.ctx.test.steps);
170
+
170
171
  if (steps && steps.length) {
171
172
  let scenarioTrace = '';
172
173
  steps.reverse().forEach((step) => {
@@ -258,3 +258,37 @@ helpers: {
258
258
  `);
259
259
  });
260
260
  };
261
+
262
+ const healTemplate = fs.readFileSync(path.join(__dirname, '../template/heal.js'), 'utf8').toString();
263
+
264
+ module.exports.heal = function (genPath) {
265
+ const testsPath = getTestRoot(genPath);
266
+
267
+ let configFile = path.join(testsPath, `codecept.conf.${extension}`);
268
+
269
+ if (!fileExists(configFile)) {
270
+ configFile = path.join(testsPath, `codecept.conf.${extension}`);
271
+ if (fileExists(configFile)) extension = 'ts';
272
+ }
273
+
274
+ output.print('Creating basic heal recipes');
275
+ output.print(`Add your own custom recipes to ./heal.${extension} file`);
276
+ output.print('Require this file in the config file and enable heal plugin:');
277
+ output.print('--------------------------');
278
+ output.print(`
279
+ require('./heal')
280
+
281
+ exports.config = {
282
+ // ...
283
+ plugins: {
284
+ heal: {
285
+ enabled: true
286
+ }
287
+ }
288
+ }
289
+ `);
290
+
291
+ const healFile = path.join(testsPath, `heal.${extension}`);
292
+ if (!safeFileWrite(healFile, healTemplate)) return;
293
+ output.success(`Heal recipes were created in ${healFile}`);
294
+ };
@@ -1,6 +1,7 @@
1
1
  // For Node version >=10.5.0, have to use experimental flag
2
2
  const { tryOrDefault } = require('../utils');
3
3
  const output = require('../output');
4
+ const store = require('../store');
4
5
  const event = require('../event');
5
6
  const Workers = require('../workers');
6
7
 
@@ -96,6 +97,8 @@ module.exports = async function (workerCount, selectedRuns, options) {
96
97
  });
97
98
 
98
99
  try {
100
+ if (options.verbose || options.debug) store.debugMode = true;
101
+
99
102
  if (options.verbose) {
100
103
  global.debugMode = true;
101
104
  const { getMachineInfo } = require('./info');
@@ -2,16 +2,19 @@ const {
2
2
  getConfig, printError, getTestRoot, createOutputDir,
3
3
  } = require('./utils');
4
4
  const Config = require('../config');
5
+ const store = require('../store');
5
6
  const Codecept = require('../codecept');
6
7
 
7
8
  module.exports = async function (test, options) {
8
9
  // registering options globally to use in config
9
10
  // Backward compatibility for --profile
11
+ // TODO: remove in CodeceptJS 4
10
12
  process.profile = options.profile;
11
13
 
12
14
  if (options.profile) {
13
15
  process.env.profile = options.profile;
14
16
  }
17
+ if (options.verbose || options.debug) store.debugMode = true;
15
18
 
16
19
  const configFile = options.config;
17
20
 
package/lib/container.js CHANGED
@@ -8,6 +8,7 @@ const recorder = require('./recorder');
8
8
  const event = require('./event');
9
9
  const WorkerStorage = require('./workerStorage');
10
10
  const store = require('./store');
11
+ const ai = require('./ai');
11
12
 
12
13
  let container = {
13
14
  helpers: {},
@@ -45,6 +46,7 @@ class Container {
45
46
  container.translation = loadTranslation(config.translation || null, config.vocabularies || []);
46
47
  container.support = createSupportObjects(config.include || {});
47
48
  container.plugins = createPlugins(config.plugins || {}, opts);
49
+ if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
48
50
  if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
49
51
  if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
50
52
  }