codeceptjs 3.7.0-beta.1 → 3.7.0-beta.10

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 (73) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +7 -0
  3. package/lib/actor.js +46 -92
  4. package/lib/ai.js +130 -121
  5. package/lib/codecept.js +2 -2
  6. package/lib/command/check.js +186 -0
  7. package/lib/command/definitions.js +3 -1
  8. package/lib/command/interactive.js +1 -1
  9. package/lib/command/run-workers.js +2 -54
  10. package/lib/command/workers/runTests.js +64 -225
  11. package/lib/container.js +27 -0
  12. package/lib/effects.js +218 -0
  13. package/lib/els.js +87 -106
  14. package/lib/event.js +18 -17
  15. package/lib/heal.js +10 -0
  16. package/lib/helper/AI.js +2 -1
  17. package/lib/helper/Appium.js +31 -22
  18. package/lib/helper/Playwright.js +22 -1
  19. package/lib/helper/Puppeteer.js +5 -0
  20. package/lib/helper/WebDriver.js +29 -8
  21. package/lib/listener/emptyRun.js +2 -5
  22. package/lib/listener/exit.js +5 -8
  23. package/lib/listener/globalTimeout.js +66 -10
  24. package/lib/listener/result.js +12 -0
  25. package/lib/listener/steps.js +3 -6
  26. package/lib/listener/store.js +9 -1
  27. package/lib/mocha/asyncWrapper.js +15 -3
  28. package/lib/mocha/cli.js +79 -28
  29. package/lib/mocha/featureConfig.js +13 -0
  30. package/lib/mocha/hooks.js +32 -3
  31. package/lib/mocha/inject.js +5 -0
  32. package/lib/mocha/scenarioConfig.js +11 -0
  33. package/lib/mocha/suite.js +27 -1
  34. package/lib/mocha/test.js +102 -3
  35. package/lib/mocha/types.d.ts +11 -0
  36. package/lib/output.js +75 -73
  37. package/lib/pause.js +3 -10
  38. package/lib/plugin/analyze.js +349 -0
  39. package/lib/plugin/autoDelay.js +2 -2
  40. package/lib/plugin/commentStep.js +5 -0
  41. package/lib/plugin/customReporter.js +52 -0
  42. package/lib/plugin/heal.js +30 -0
  43. package/lib/plugin/pageInfo.js +140 -0
  44. package/lib/plugin/retryTo.js +18 -118
  45. package/lib/plugin/screenshotOnFail.js +12 -17
  46. package/lib/plugin/standardActingHelpers.js +4 -1
  47. package/lib/plugin/stepByStepReport.js +6 -5
  48. package/lib/plugin/stepTimeout.js +1 -1
  49. package/lib/plugin/tryTo.js +17 -107
  50. package/lib/recorder.js +5 -5
  51. package/lib/rerun.js +43 -42
  52. package/lib/result.js +161 -0
  53. package/lib/step/base.js +228 -0
  54. package/lib/step/config.js +50 -0
  55. package/lib/step/func.js +46 -0
  56. package/lib/step/helper.js +50 -0
  57. package/lib/step/meta.js +99 -0
  58. package/lib/step/record.js +74 -0
  59. package/lib/step/retry.js +11 -0
  60. package/lib/step/section.js +55 -0
  61. package/lib/step.js +20 -347
  62. package/lib/steps.js +50 -0
  63. package/lib/store.js +4 -0
  64. package/lib/timeout.js +66 -0
  65. package/lib/utils.js +93 -0
  66. package/lib/within.js +2 -2
  67. package/lib/workers.js +29 -49
  68. package/package.json +23 -20
  69. package/typings/index.d.ts +5 -4
  70. package/typings/promiseBasedTypes.d.ts +617 -7
  71. package/typings/types.d.ts +663 -34
  72. package/lib/listener/artifacts.js +0 -19
  73. package/lib/plugin/debugErrors.js +0 -67
package/README.md CHANGED
@@ -3,16 +3,15 @@
3
3
  [<img src="https://img.shields.io/badge/slack-@codeceptjs-purple.svg?logo=slack">](https://join.slack.com/t/codeceptjs/shared_invite/enQtMzA5OTM4NDM2MzA4LWE4MThhN2NmYTgxNTU5MTc4YzAyYWMwY2JkMmZlYWI5MWQ2MDM5MmRmYzZmYmNiNmY5NTAzM2EwMGIwOTNhOGQ) [<img src="https://img.shields.io/badge/discourse-codeceptjs-purple">](https://codecept.discourse.group) [![NPM version][npm-image]][npm-url] [<img src="https://img.shields.io/badge/dockerhub-images-blue.svg?logo=codeceptjs">](https://hub.docker.com/r/codeceptjs/codeceptjs)
4
4
  [![AI features](https://img.shields.io/badge/AI-features?logo=openai&logoColor=white)](https://github.com/codeceptjs/CodeceptJS/edit/3.x/docs/ai.md) [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)
5
5
 
6
- Build Status:
6
+ ## Build Status
7
7
 
8
- Appium Helper:
9
- [![Appium V2 Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml)
10
-
11
- Web Helper:
12
- [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml)
13
- [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml)
14
- [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml)
15
- [![TestCafe Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml)
8
+ | Type | Engine | Status |
9
+ | --------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | 🌐 Web | Playwright | [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) |
11
+ | 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
12
+ | 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
13
+ | 🌐 Web | TestCafe | [![TestCafe Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml) |
14
+ | 📱 Mobile | Appium | [![Appium Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml) |
16
15
 
17
16
  # CodeceptJS [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua)
18
17
 
@@ -292,7 +291,7 @@ When using Typescript, replace `module.exports` with `export` for autocompletion
292
291
 
293
292
  Thanks to our awesome contributors! 🎉
294
293
  <a href="https://github.com/codeceptjs/codeceptjs/graphs/contributors">
295
- <img src="https://contrib.rocks/image?repo=codeceptjs/codeceptjs" />
294
+ <img src="https://contrib.rocks/image?repo=codeceptjs/codeceptjs" />
296
295
  </a>
297
296
 
298
297
  Made with [contrib.rocks](https://contrib.rocks).
package/bin/codecept.js CHANGED
@@ -58,6 +58,13 @@ program
58
58
  .description('Creates dummy config in current dir or [path]')
59
59
  .action(errorHandler(require('../lib/command/init')))
60
60
 
61
+ program
62
+ .command('check')
63
+ .option(commandFlags.config.flag, commandFlags.config.description)
64
+ .description('Checks configuration and environment before running tests')
65
+ .option('-t, --timeout [ms]', 'timeout for checks in ms, 20000 by default')
66
+ .action(errorHandler(require('../lib/command/check')))
67
+
61
68
  program
62
69
  .command('migrate [path]')
63
70
  .description('Migrate json config to js config in current dir or [path]')
package/lib/actor.js CHANGED
@@ -1,11 +1,12 @@
1
- const Step = require('./step');
2
- const { MetaStep } = require('./step');
3
- const container = require('./container');
4
- const { methodsOfObject } = require('./utils');
5
- const recorder = require('./recorder');
6
- const event = require('./event');
7
- const store = require('./store');
8
- const output = require('./output');
1
+ const Step = require('./step')
2
+ const MetaStep = require('./step/meta')
3
+ const recordStep = require('./step/record')
4
+ const container = require('./container')
5
+ const { methodsOfObject } = require('./utils')
6
+ const { TIMEOUT_ORDER } = require('./timeout')
7
+ const event = require('./event')
8
+ const store = require('./store')
9
+ const output = require('./output')
9
10
 
10
11
  /**
11
12
  * @interface
@@ -21,13 +22,13 @@ class Actor {
21
22
  * ⚠️ returns a promise which is synchronized internally by recorder
22
23
  */
23
24
  async say(msg, color = 'cyan') {
24
- const step = new Step('say', 'say');
25
- step.status = 'passed';
25
+ const step = new Step('say', 'say')
26
+ step.status = 'passed'
26
27
  return recordStep(step, [msg]).then(() => {
27
28
  // this is backward compatibility as this event may be used somewhere
28
- event.emit(event.step.comment, msg);
29
- output.say(msg, `${color}`);
30
- });
29
+ event.emit(event.step.comment, msg)
30
+ output.say(msg, `${color}`)
31
+ })
31
32
  }
32
33
 
33
34
  /**
@@ -38,14 +39,16 @@ class Actor {
38
39
  * @inner
39
40
  */
40
41
  limitTime(timeout) {
41
- if (!store.timeouts) return this;
42
+ if (!store.timeouts) return this
43
+
44
+ console.log('I.limitTime() is deprecated, use step.timeout() instead')
42
45
 
43
46
  event.dispatcher.prependOnceListener(event.step.before, step => {
44
- output.log(`Timeout to ${step}: ${timeout}s`);
45
- step.setTimeout(timeout * 1000, Step.TIMEOUT_ORDER.codeLimitTime);
46
- });
47
+ output.log(`Timeout to ${step}: ${timeout}s`)
48
+ step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime)
49
+ })
47
50
 
48
- return this;
51
+ return this
49
52
  }
50
53
 
51
54
  /**
@@ -55,11 +58,10 @@ class Actor {
55
58
  * @inner
56
59
  */
57
60
  retry(opts) {
58
- if (opts === undefined) opts = 1;
59
- recorder.retry(opts);
60
- // remove retry once the step passed
61
- recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop()));
62
- return this;
61
+ console.log('I.retry() is deprecated, use step.retry() instead')
62
+ const retryStep = require('./step/retry')
63
+ retryStep(opts)
64
+ return this
63
65
  }
64
66
  }
65
67
 
@@ -70,102 +72,54 @@ class Actor {
70
72
  * @ignore
71
73
  */
72
74
  module.exports = function (obj = {}) {
73
- const actor = container.actor() || new Actor();
75
+ const actor = container.actor() || new Actor()
74
76
 
75
77
  // load all helpers once container initialized
76
78
  container.started(() => {
77
- const translation = container.translation();
78
- const helpers = container.helpers();
79
+ const translation = container.translation()
80
+ const helpers = container.helpers()
79
81
 
80
82
  // add methods from enabled helpers
81
83
  Object.values(helpers).forEach(helper => {
82
84
  methodsOfObject(helper, 'Helper')
83
85
  .filter(method => method !== 'constructor' && method[0] !== '_')
84
86
  .forEach(action => {
85
- const actionAlias = translation.actionAliasFor(action);
87
+ const actionAlias = translation.actionAliasFor(action)
86
88
  if (!actor[action]) {
87
89
  actor[action] = actor[actionAlias] = function () {
88
- const step = new Step(helper, action);
90
+ const step = new Step(helper, action)
89
91
  if (translation.loaded) {
90
- step.name = actionAlias;
91
- step.actor = translation.I;
92
+ step.name = actionAlias
93
+ step.actor = translation.I
92
94
  }
93
95
  // add methods to promise chain
94
- return recordStep(step, Array.from(arguments));
95
- };
96
+ return recordStep(step, Array.from(arguments))
97
+ }
96
98
  }
97
- });
98
- });
99
+ })
100
+ })
99
101
 
100
102
  // add translated custom steps from actor
101
103
  Object.keys(obj).forEach(key => {
102
- const actionAlias = translation.actionAliasFor(key);
104
+ const actionAlias = translation.actionAliasFor(key)
103
105
  if (!actor[actionAlias]) {
104
- actor[actionAlias] = actor[key];
106
+ actor[actionAlias] = actor[key]
105
107
  }
106
- });
108
+ })
107
109
 
108
110
  container.append({
109
111
  support: {
110
112
  I: actor,
111
113
  },
112
- });
113
- });
114
+ })
115
+ })
114
116
  // store.actor = actor;
115
117
  // add custom steps from actor
116
118
  Object.keys(obj).forEach(key => {
117
- const ms = new MetaStep('I', key);
118
- ms.setContext(actor);
119
- actor[key] = ms.run.bind(ms, obj[key]);
120
- });
121
-
122
- return actor;
123
- };
124
-
125
- function recordStep(step, args) {
126
- step.status = 'queued';
127
- step.setArguments(args);
128
-
129
- // run async before step hooks
130
- event.emit(event.step.before, step);
131
-
132
- const task = `${step.name}: ${step.humanizeArgs()}`;
133
- let val;
134
-
135
- // run step inside promise
136
- recorder.add(
137
- task,
138
- () => {
139
- if (!step.startTime) {
140
- // step can be retries
141
- event.emit(event.step.started, step);
142
- step.startTime = Date.now();
143
- }
144
- return (val = step.run(...args));
145
- },
146
- false,
147
- undefined,
148
- step.getTimeout(),
149
- );
150
-
151
- event.emit(event.step.after, step);
152
-
153
- recorder.add('step passed', () => {
154
- step.endTime = Date.now();
155
- event.emit(event.step.passed, step, val);
156
- event.emit(event.step.finished, step);
157
- });
158
-
159
- recorder.catchWithoutStop(err => {
160
- step.status = 'failed';
161
- step.endTime = Date.now();
162
- event.emit(event.step.failed, step);
163
- event.emit(event.step.finished, step);
164
- throw err;
165
- });
166
-
167
- recorder.add('return result', () => val);
168
- // run async after step hooks
119
+ const ms = new MetaStep('I', key)
120
+ ms.setContext(actor)
121
+ actor[key] = ms.run.bind(ms, obj[key])
122
+ })
169
123
 
170
- return recorder.promise();
124
+ return actor
171
125
  }
package/lib/ai.js CHANGED
@@ -1,40 +1,44 @@
1
- const debug = require('debug')('codeceptjs:ai');
2
- const output = require('./output');
3
- const event = require('./event');
4
- const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html');
1
+ const debug = require('debug')('codeceptjs:ai')
2
+ const output = require('./output')
3
+ const event = require('./event')
4
+ const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html')
5
5
 
6
6
  const defaultHtmlConfig = {
7
7
  maxLength: 50000,
8
8
  simplify: true,
9
9
  minify: true,
10
10
  html: {},
11
- };
11
+ }
12
12
 
13
13
  const defaultPrompts = {
14
- writeStep: (html, input) => [{
15
- role: 'user',
16
- content: `I am test engineer writing test in CodeceptJS
14
+ writeStep: (html, input) => [
15
+ {
16
+ role: 'user',
17
+ content: `I am test engineer writing test in CodeceptJS
17
18
  I have opened web page and I want to use CodeceptJS to ${input} on this page
18
19
  Provide me valid CodeceptJS code to accomplish it
19
20
  Use only locators from this HTML: \n\n${html}`,
20
- },
21
+ },
21
22
  ],
22
23
 
23
24
  healStep: (html, { step, error, prevSteps }) => {
24
- return [{
25
- role: 'user',
26
- content: `As a test automation engineer I am testing web application using CodeceptJS.
25
+ return [
26
+ {
27
+ role: 'user',
28
+ content: `As a test automation engineer I am testing web application using CodeceptJS.
27
29
  I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
28
30
  Propose how to adjust ${step.toCode()} step to fix the test.
29
31
  Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
30
32
  Here is the error message: ${error.message}
31
33
  Here is HTML code of a page where the failure has happened: \n\n${html}`,
32
- }];
34
+ },
35
+ ]
33
36
  },
34
37
 
35
- generatePageObject: (html, extraPrompt = '', rootLocator = null) => [{
36
- role: 'user',
37
- content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
38
+ generatePageObject: (html, extraPrompt = '', rootLocator = null) => [
39
+ {
40
+ role: 'user',
41
+ content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
38
42
  Here is an sample page object:
39
43
 
40
44
  const { I } = inject();
@@ -60,72 +64,73 @@ module.exports = {
60
64
  ${extraPrompt}
61
65
  ${rootLocator ? `All provided elements are inside '${rootLocator}'. Declare it as root variable and for every locator use locate(...).inside(root)` : ''}
62
66
  Add only locators from this HTML: \n\n${html}`,
63
- }],
64
- };
67
+ },
68
+ ],
69
+ }
65
70
 
66
71
  class AiAssistant {
67
72
  constructor() {
68
- this.totalTime = 0;
69
- this.numTokens = 0;
73
+ this.totalTime = 0
74
+ this.numTokens = 0
70
75
 
71
- this.reset();
72
- this.connectToEvents();
76
+ this.reset()
77
+ this.connectToEvents()
73
78
  }
74
79
 
75
80
  enable(config = {}) {
76
- debug('Enabling AI assistant');
77
- this.isEnabled = true;
81
+ debug('Enabling AI assistant')
82
+ this.isEnabled = true
78
83
 
79
- const { html, prompts, ...aiConfig } = config;
84
+ const { html, prompts, ...aiConfig } = config
80
85
 
81
- this.config = Object.assign(this.config, aiConfig);
82
- this.htmlConfig = Object.assign(defaultHtmlConfig, html);
83
- this.prompts = Object.assign(defaultPrompts, prompts);
86
+ this.config = Object.assign(this.config, aiConfig)
87
+ this.htmlConfig = Object.assign(defaultHtmlConfig, html)
88
+ this.prompts = Object.assign(defaultPrompts, prompts)
84
89
 
85
- debug('Config', this.config);
90
+ debug('Config', this.config)
86
91
  }
87
92
 
88
93
  reset() {
89
- this.numTokens = 0;
90
- this.isEnabled = false;
94
+ this.numTokens = 0
95
+ this.isEnabled = false
91
96
  this.config = {
92
97
  maxTokens: 1000000,
93
98
  request: null,
94
99
  response: parseCodeBlocks,
95
100
  // lets limit token usage to 1M
96
- };
97
- this.minifiedHtml = null;
98
- this.response = null;
99
- this.totalTime = 0;
101
+ }
102
+ this.minifiedHtml = null
103
+ this.response = null
104
+ this.totalTime = 0
100
105
  }
101
106
 
102
107
  disable() {
103
- this.isEnabled = false;
108
+ this.isEnabled = false
104
109
  }
105
110
 
106
111
  connectToEvents() {
107
112
  event.dispatcher.on(event.all.result, () => {
108
113
  if (this.isEnabled && this.numTokens > 0) {
109
- const numTokensK = Math.ceil(this.numTokens / 1000);
110
- const maxTokensK = Math.ceil(this.config.maxTokens / 1000);
111
- output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`);
114
+ const numTokensK = Math.ceil(this.numTokens / 1000)
115
+ const maxTokensK = Math.ceil(this.config.maxTokens / 1000)
116
+ output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`)
112
117
  }
113
- });
118
+ })
114
119
  }
115
120
 
116
121
  checkRequestFn() {
117
122
  if (!this.isEnabled) {
118
- debug('AI assistant is disabled');
119
- return;
123
+ debug('AI assistant is disabled')
124
+ return
120
125
  }
121
126
 
122
- if (this.config.request) return;
127
+ if (this.config.request) return
123
128
 
124
129
  const noRequestErrorMessage = `
125
- No request function is set for AI assistant.
126
- Please implement your own request function and set it in the config.
130
+ No request function is set for AI assistant.
127
131
 
128
- [!] 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.
132
+ [!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service.
133
+ Please implement your own request function and set it in the config.
129
134
 
130
135
  Example (connect to OpenAI):
131
136
 
@@ -134,82 +139,80 @@ class AiAssistant {
134
139
  const OpenAI = require('openai');
135
140
  const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] })
136
141
  const response = await openai.chat.completions.create({
137
- model: 'gpt-3.5-turbo-0125',
142
+ model: 'gpt-4o-mini',
138
143
  messages,
139
144
  });
140
145
  return response?.data?.choices[0]?.message?.content;
141
146
  }
142
147
  }
143
- `.trim();
148
+ `.trim()
144
149
 
145
- throw new Error(noRequestErrorMessage);
150
+ throw new Error(noRequestErrorMessage)
146
151
  }
147
152
 
148
153
  async setHtmlContext(html) {
149
- let processedHTML = html;
154
+ let processedHTML = html
150
155
 
151
156
  if (this.htmlConfig.simplify) {
152
- processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
157
+ processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig)
153
158
  }
154
159
 
155
- if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
156
- if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
160
+ if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML)
161
+ if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0]
157
162
 
158
- this.minifiedHtml = processedHTML;
163
+ this.minifiedHtml = processedHTML
159
164
  }
160
165
 
161
166
  getResponse() {
162
- return this.response || '';
167
+ return this.response || ''
163
168
  }
164
169
 
165
170
  async createCompletion(messages) {
166
- if (!this.isEnabled) return '';
167
-
168
- debug('Request', messages);
169
-
170
- this.checkRequestFn();
171
-
172
- this.response = null;
173
-
174
- this.calculateTokens(messages);
171
+ if (!this.isEnabled) return ''
175
172
 
176
173
  try {
177
- const startTime = process.hrtime();
178
- this.response = await this.config.request(messages);
179
- const endTime = process.hrtime(startTime);
180
- const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9;
181
-
182
- this.totalTime += Math.round(executionTimeInSeconds);
183
- debug('AI response time', executionTimeInSeconds);
184
- debug('Response', this.response);
185
- this.stopWhenReachingTokensLimit();
186
- return this.response;
174
+ this.checkRequestFn()
175
+ debug('Request', messages)
176
+
177
+ this.response = null
178
+
179
+ this.calculateTokens(messages)
180
+ const startTime = process.hrtime()
181
+ this.response = await this.config.request(messages)
182
+ const endTime = process.hrtime(startTime)
183
+ const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9
184
+
185
+ this.totalTime += Math.round(executionTimeInSeconds)
186
+ debug('AI response time', executionTimeInSeconds)
187
+ debug('Response', this.response)
188
+ this.stopWhenReachingTokensLimit()
189
+ return this.response
187
190
  } catch (err) {
188
- debug(err.response);
189
- output.print('');
190
- output.error(`AI service error: ${err.message}`);
191
- if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code);
192
- if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message);
193
- this.stopWhenReachingTokensLimit();
194
- return '';
191
+ debug(err.response)
192
+ output.print('')
193
+ output.error(`AI service error: ${err.message}`)
194
+ if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code)
195
+ if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message)
196
+ this.stopWhenReachingTokensLimit()
197
+ return ''
195
198
  }
196
199
  }
197
200
 
198
201
  async healFailedStep(failureContext) {
199
- if (!this.isEnabled) return [];
200
- if (!failureContext.html) throw new Error('No HTML context provided');
202
+ if (!this.isEnabled) return []
203
+ if (!failureContext.html) throw new Error('No HTML context provided')
201
204
 
202
- await this.setHtmlContext(failureContext.html);
205
+ await this.setHtmlContext(failureContext.html)
203
206
 
204
207
  if (!this.minifiedHtml) {
205
- debug('HTML context is empty after removing non-interactive elements & minification');
206
- return [];
208
+ debug('HTML context is empty after removing non-interactive elements & minification')
209
+ return []
207
210
  }
208
211
 
209
- const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext));
210
- if (!response) return [];
212
+ const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext))
213
+ if (!response) return []
211
214
 
212
- return this.config.response(response);
215
+ return this.config.response(response)
213
216
  }
214
217
 
215
218
  /**
@@ -219,13 +222,13 @@ class AiAssistant {
219
222
  * @returns
220
223
  */
221
224
  async generatePageObject(extraPrompt = null, locator = null) {
222
- if (!this.isEnabled) return [];
223
- if (!this.minifiedHtml) throw new Error('No HTML context provided');
225
+ if (!this.isEnabled) return []
226
+ if (!this.minifiedHtml) throw new Error('No HTML context provided')
224
227
 
225
- const response = await this.createCompletion(this.prompts.generatePageObject(this.minifiedHtml, locator, extraPrompt));
226
- if (!response) return [];
228
+ const response = await this.createCompletion(this.prompts.generatePageObject(this.minifiedHtml, locator, extraPrompt))
229
+ if (!response) return []
227
230
 
228
- return this.config.response(response);
231
+ return this.config.response(response)
229
232
  }
230
233
 
231
234
  calculateTokens(messages) {
@@ -233,66 +236,72 @@ class AiAssistant {
233
236
  // this approach was tested via https://platform.openai.com/tokenizer
234
237
  // we need it to display current tokens usage so users could analyze effectiveness of AI
235
238
 
236
- const inputString = messages.map(m => m.content).join(' ').trim();
237
- const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length;
239
+ const inputString = messages
240
+ .map(m => m.content)
241
+ .join(' ')
242
+ .trim()
243
+ const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length
238
244
 
239
245
  // 2.5 token is constant for average HTML input
240
- const tokens = numWords * 2.5;
246
+ const tokens = numWords * 2.5
241
247
 
242
- this.numTokens += tokens;
248
+ this.numTokens += tokens
243
249
 
244
- return tokens;
250
+ return tokens
245
251
  }
246
252
 
247
253
  stopWhenReachingTokensLimit() {
248
- if (this.numTokens < this.config.maxTokens) return;
254
+ if (this.numTokens < this.config.maxTokens) return
249
255
 
250
- output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`);
251
- this.disable();
256
+ output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`)
257
+ this.disable()
252
258
  }
253
259
 
254
260
  async writeSteps(input) {
255
- if (!this.isEnabled) return;
256
- if (!this.minifiedHtml) throw new Error('No HTML context provided');
261
+ if (!this.isEnabled) return
262
+ if (!this.minifiedHtml) throw new Error('No HTML context provided')
257
263
 
258
- const snippets = [];
264
+ const snippets = []
259
265
 
260
- const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input));
261
- if (!response) return;
262
- snippets.push(...this.config.response(response));
266
+ const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input))
267
+ if (!response) return
268
+ snippets.push(...this.config.response(response))
263
269
 
264
- debug(snippets[0]);
270
+ debug(snippets[0])
265
271
 
266
- return snippets[0];
272
+ return snippets[0]
267
273
  }
268
274
  }
269
275
 
270
276
  function parseCodeBlocks(response) {
271
277
  // Regular expression pattern to match code snippets
272
- const codeSnippetPattern = /```(?:javascript|js|typescript|ts)?\n([\s\S]+?)\n```/g;
278
+ const codeSnippetPattern = /```(?:javascript|js|typescript|ts)?\n([\s\S]+?)\n```/g
273
279
 
274
280
  // Array to store extracted code snippets
275
- const codeSnippets = [];
281
+ const codeSnippets = []
276
282
 
277
- response = response.split('\n').map(line => line.trim()).join('\n');
283
+ response = response
284
+ .split('\n')
285
+ .map(line => line.trim())
286
+ .join('\n')
278
287
 
279
288
  // Iterate over matches and extract code snippets
280
- let match;
289
+ let match
281
290
  while ((match = codeSnippetPattern.exec(response)) !== null) {
282
- codeSnippets.push(match[1]);
291
+ codeSnippets.push(match[1])
283
292
  }
284
293
 
285
294
  // Remove "Scenario", "Feature", and "require()" lines
286
295
  const modifiedSnippets = codeSnippets.map(snippet => {
287
- const lines = snippet.split('\n');
296
+ const lines = snippet.split('\n')
288
297
 
289
- const filteredLines = lines.filter(line => !line.includes('I.amOnPage') && !line.startsWith('Scenario') && !line.startsWith('Feature') && !line.includes('= require('));
298
+ const filteredLines = lines.filter(line => !line.includes('I.amOnPage') && !line.startsWith('Scenario') && !line.startsWith('Feature') && !line.includes('= require('))
290
299
 
291
- return filteredLines.join('\n');
300
+ return filteredLines.join('\n')
292
301
  // remove snippets that move from current url
293
- }); // .filter(snippet => !line.includes('I.amOnPage'));
302
+ }) // .filter(snippet => !line.includes('I.amOnPage'));
294
303
 
295
- return modifiedSnippets.filter(snippet => !!snippet);
304
+ return modifiedSnippets.filter(snippet => !!snippet)
296
305
  }
297
306
 
298
- module.exports = new AiAssistant();
307
+ module.exports = new AiAssistant()