codeceptjs 4.0.0-beta.4 → 4.0.0-beta.5

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 (150) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +139 -87
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/generate.js +10 -14
  17. package/lib/command/gherkin/snippets.js +75 -73
  18. package/lib/command/gherkin/steps.js +1 -1
  19. package/lib/command/info.js +42 -8
  20. package/lib/command/init.js +13 -12
  21. package/lib/command/interactive.js +10 -2
  22. package/lib/command/list.js +1 -1
  23. package/lib/command/run-multiple/chunk.js +48 -45
  24. package/lib/command/run-multiple.js +12 -35
  25. package/lib/command/run-workers.js +21 -58
  26. package/lib/command/utils.js +5 -6
  27. package/lib/command/workers/runTests.js +262 -220
  28. package/lib/container.js +386 -238
  29. package/lib/data/context.js +10 -13
  30. package/lib/data/dataScenarioConfig.js +8 -8
  31. package/lib/data/dataTableArgument.js +6 -6
  32. package/lib/data/table.js +5 -11
  33. package/lib/effects.js +223 -0
  34. package/lib/element/WebElement.js +327 -0
  35. package/lib/els.js +158 -0
  36. package/lib/event.js +21 -17
  37. package/lib/heal.js +88 -80
  38. package/lib/helper/AI.js +2 -1
  39. package/lib/helper/ApiDataFactory.js +3 -6
  40. package/lib/helper/Appium.js +47 -51
  41. package/lib/helper/FileSystem.js +3 -3
  42. package/lib/helper/GraphQLDataFactory.js +3 -3
  43. package/lib/helper/JSONResponse.js +75 -37
  44. package/lib/helper/Mochawesome.js +31 -9
  45. package/lib/helper/Nightmare.js +35 -53
  46. package/lib/helper/Playwright.js +262 -267
  47. package/lib/helper/Protractor.js +54 -77
  48. package/lib/helper/Puppeteer.js +246 -260
  49. package/lib/helper/REST.js +5 -17
  50. package/lib/helper/TestCafe.js +21 -44
  51. package/lib/helper/WebDriver.js +151 -170
  52. package/lib/helper/extras/Popup.js +22 -22
  53. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  54. package/lib/listener/emptyRun.js +55 -0
  55. package/lib/listener/exit.js +7 -10
  56. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  57. package/lib/listener/globalTimeout.js +165 -0
  58. package/lib/listener/helpers.js +15 -15
  59. package/lib/listener/mocha.js +1 -1
  60. package/lib/listener/result.js +12 -0
  61. package/lib/listener/retryEnhancer.js +85 -0
  62. package/lib/listener/steps.js +32 -18
  63. package/lib/listener/store.js +20 -0
  64. package/lib/mocha/asyncWrapper.js +231 -0
  65. package/lib/{interfaces → mocha}/bdd.js +3 -3
  66. package/lib/mocha/cli.js +308 -0
  67. package/lib/mocha/factory.js +104 -0
  68. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  69. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  70. package/lib/mocha/hooks.js +112 -0
  71. package/lib/mocha/index.js +12 -0
  72. package/lib/mocha/inject.js +29 -0
  73. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  74. package/lib/mocha/suite.js +82 -0
  75. package/lib/mocha/test.js +181 -0
  76. package/lib/mocha/types.d.ts +42 -0
  77. package/lib/mocha/ui.js +232 -0
  78. package/lib/output.js +82 -62
  79. package/lib/pause.js +160 -138
  80. package/lib/plugin/analyze.js +396 -0
  81. package/lib/plugin/auth.js +435 -0
  82. package/lib/plugin/autoDelay.js +8 -8
  83. package/lib/plugin/autoLogin.js +3 -338
  84. package/lib/plugin/commentStep.js +6 -1
  85. package/lib/plugin/coverage.js +10 -19
  86. package/lib/plugin/customLocator.js +3 -3
  87. package/lib/plugin/customReporter.js +52 -0
  88. package/lib/plugin/eachElement.js +1 -1
  89. package/lib/plugin/fakerTransform.js +1 -1
  90. package/lib/plugin/heal.js +36 -9
  91. package/lib/plugin/htmlReporter.js +1947 -0
  92. package/lib/plugin/pageInfo.js +140 -0
  93. package/lib/plugin/retryFailedStep.js +17 -18
  94. package/lib/plugin/retryTo.js +2 -113
  95. package/lib/plugin/screenshotOnFail.js +17 -58
  96. package/lib/plugin/selenoid.js +15 -35
  97. package/lib/plugin/standardActingHelpers.js +4 -1
  98. package/lib/plugin/stepByStepReport.js +56 -17
  99. package/lib/plugin/stepTimeout.js +5 -12
  100. package/lib/plugin/subtitles.js +4 -4
  101. package/lib/plugin/tryTo.js +3 -102
  102. package/lib/plugin/wdio.js +8 -10
  103. package/lib/recorder.js +155 -124
  104. package/lib/rerun.js +43 -42
  105. package/lib/result.js +161 -0
  106. package/lib/secret.js +1 -1
  107. package/lib/step/base.js +239 -0
  108. package/lib/step/comment.js +10 -0
  109. package/lib/step/config.js +50 -0
  110. package/lib/step/func.js +46 -0
  111. package/lib/step/helper.js +50 -0
  112. package/lib/step/meta.js +99 -0
  113. package/lib/step/record.js +74 -0
  114. package/lib/step/retry.js +11 -0
  115. package/lib/step/section.js +55 -0
  116. package/lib/step.js +21 -332
  117. package/lib/steps.js +50 -0
  118. package/lib/store.js +37 -5
  119. package/lib/template/heal.js +2 -11
  120. package/lib/test-server.js +323 -0
  121. package/lib/timeout.js +66 -0
  122. package/lib/utils.js +351 -218
  123. package/lib/within.js +75 -55
  124. package/lib/workerStorage.js +2 -1
  125. package/lib/workers.js +386 -276
  126. package/package.json +76 -70
  127. package/translations/de-DE.js +4 -3
  128. package/translations/fr-FR.js +4 -3
  129. package/translations/index.js +1 -0
  130. package/translations/it-IT.js +4 -3
  131. package/translations/ja-JP.js +4 -3
  132. package/translations/nl-NL.js +76 -0
  133. package/translations/pl-PL.js +4 -3
  134. package/translations/pt-BR.js +4 -3
  135. package/translations/ru-RU.js +4 -3
  136. package/translations/utils.js +9 -0
  137. package/translations/zh-CN.js +4 -3
  138. package/translations/zh-TW.js +4 -3
  139. package/typings/index.d.ts +188 -186
  140. package/typings/promiseBasedTypes.d.ts +18 -705
  141. package/typings/types.d.ts +301 -804
  142. package/lib/cli.js +0 -256
  143. package/lib/helper/ExpectHelper.js +0 -391
  144. package/lib/helper/SoftExpectHelper.js +0 -381
  145. package/lib/listener/artifacts.js +0 -19
  146. package/lib/listener/timeout.js +0 -109
  147. package/lib/mochaFactory.js +0 -113
  148. package/lib/plugin/debugErrors.js +0 -67
  149. package/lib/scenario.js +0 -224
  150. package/lib/ui.js +0 -236
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
42
43
 
43
- 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
- });
44
+ console.log('I.limitTime() is deprecated, use step.timeout() instead')
47
45
 
48
- return this;
46
+ event.dispatcher.prependOnceListener(event.step.before, step => {
47
+ output.log(`Timeout to ${step}: ${timeout}s`)
48
+ step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime)
49
+ })
50
+
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,92 +72,54 @@ class Actor {
70
72
  * @ignore
71
73
  */
72
74
  module.exports = function (obj = {}) {
73
- if (!store.actor) {
74
- store.actor = new Actor();
75
- }
76
- const actor = store.actor;
77
-
78
- const translation = container.translation();
79
-
80
- if (Object.keys(obj).length > 0) {
81
- Object.keys(obj)
82
- .forEach(action => {
83
- const actionAlias = translation.actionAliasFor(action);
75
+ const actor = container.actor() || new Actor()
84
76
 
85
- const currentMethod = obj[action];
86
- const ms = new MetaStep('I', action);
87
- if (translation.loaded) {
88
- ms.name = actionAlias;
89
- ms.actor = translation.I;
90
- }
91
- ms.setContext(actor);
92
- actor[action] = actor[actionAlias] = ms.run.bind(ms, currentMethod);
93
- });
94
- }
95
-
96
- const helpers = container.helpers();
77
+ // load all helpers once container initialized
78
+ container.started(() => {
79
+ const translation = container.translation()
80
+ const helpers = container.helpers()
97
81
 
98
- // add methods from enabled helpers
99
- Object.values(helpers)
100
- .forEach((helper) => {
82
+ // add methods from enabled helpers
83
+ Object.values(helpers).forEach(helper => {
101
84
  methodsOfObject(helper, 'Helper')
102
85
  .filter(method => method !== 'constructor' && method[0] !== '_')
103
- .forEach((action) => {
104
- const actionAlias = translation.actionAliasFor(action);
86
+ .forEach(action => {
87
+ const actionAlias = translation.actionAliasFor(action)
105
88
  if (!actor[action]) {
106
89
  actor[action] = actor[actionAlias] = function () {
107
- const step = new Step(helper, action);
90
+ const step = new Step(helper, action)
108
91
  if (translation.loaded) {
109
- step.name = actionAlias;
110
- step.actor = translation.I;
92
+ step.name = actionAlias
93
+ step.actor = translation.I
111
94
  }
112
95
  // add methods to promise chain
113
- return recordStep(step, Array.from(arguments));
114
- };
96
+ return recordStep(step, Array.from(arguments))
97
+ }
115
98
  }
116
- });
117
- });
118
-
119
- return actor;
120
- };
121
-
122
- function recordStep(step, args) {
123
- step.status = 'queued';
124
- step.setArguments(args);
125
-
126
- // run async before step hooks
127
- event.emit(event.step.before, step);
128
-
129
- const task = `${step.name}: ${step.humanizeArgs()}`;
130
- let val;
131
-
132
- // run step inside promise
133
- recorder.add(task, () => {
134
- if (!step.startTime) { // step can be retries
135
- event.emit(event.step.started, step);
136
- step.startTime = Date.now();
137
- }
138
- return val = step.run(...args);
139
- }, false, undefined, step.getTimeout());
140
-
141
- event.emit(event.step.after, step);
142
-
143
- recorder.add('step passed', () => {
144
- step.endTime = Date.now();
145
- event.emit(event.step.passed, step, val);
146
- event.emit(event.step.finished, step);
147
- });
148
-
149
- recorder.catchWithoutStop((err) => {
150
- step.status = 'failed';
151
- step.endTime = Date.now();
152
- event.emit(event.step.failed, step);
153
- event.emit(event.step.finished, step);
154
- throw err;
155
- });
156
-
157
- recorder.add('return result', () => val);
158
- // run async after step hooks
159
-
160
- return recorder.promise();
99
+ })
100
+ })
101
+
102
+ // add translated custom steps from actor
103
+ Object.keys(obj).forEach(key => {
104
+ const actionAlias = translation.actionAliasFor(key)
105
+ if (!actor[actionAlias]) {
106
+ actor[actionAlias] = actor[key]
107
+ }
108
+ })
109
+
110
+ container.append({
111
+ support: {
112
+ I: actor,
113
+ },
114
+ })
115
+ })
116
+ // store.actor = actor;
117
+ // add custom steps from actor
118
+ Object.keys(obj).forEach(key => {
119
+ const ms = new MetaStep('I', key)
120
+ ms.setContext(actor)
121
+ actor[key] = ms.run.bind(ms, obj[key])
122
+ })
123
+
124
+ return actor
161
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()
@@ -5,7 +5,7 @@ const output = require('../output')
5
5
 
6
6
  class EmptinessAssertion extends Assertion {
7
7
  constructor(params) {
8
- super((value) => {
8
+ super(value => {
9
9
  if (Array.isArray(value)) {
10
10
  return value.length === 0
11
11
  }
@@ -22,9 +22,7 @@ class EmptinessAssertion extends Assertion {
22
22
  const err = new AssertionFailedError(this.params, "{{customMessage}}expected {{subject}} '{{value}}' {{type}}")
23
23
 
24
24
  err.cliMessage = () => {
25
- const msg = err.template
26
- .replace('{{value}}', output.colors.bold('{{value}}'))
27
- .replace('{{subject}}', output.colors.bold('{{subject}}'))
25
+ const msg = err.template.replace('{{value}}', output.colors.bold('{{value}}')).replace('{{subject}}', output.colors.bold('{{subject}}'))
28
26
  return template(msg, this.params)
29
27
  }
30
28
  return err
@@ -39,5 +37,5 @@ class EmptinessAssertion extends Assertion {
39
37
 
40
38
  module.exports = {
41
39
  Assertion: EmptinessAssertion,
42
- empty: (subject) => new EmptinessAssertion({ subject }),
40
+ empty: subject => new EmptinessAssertion({ subject }),
43
41
  }
@@ -18,10 +18,7 @@ class EqualityAssertion extends Assertion {
18
18
  getException() {
19
19
  const params = this.params
20
20
  params.jar = template(params.jar, params)
21
- const err = new AssertionFailedError(
22
- params,
23
- '{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"',
24
- )
21
+ const err = new AssertionFailedError(params, '{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"')
25
22
  err.showDiff = false
26
23
  if (typeof err.cliMessage === 'function') {
27
24
  err.message = err.cliMessage()
@@ -42,8 +39,8 @@ class EqualityAssertion extends Assertion {
42
39
 
43
40
  module.exports = {
44
41
  Assertion: EqualityAssertion,
45
- equals: (jar) => new EqualityAssertion({ jar }),
46
- urlEquals: (baseUrl) => {
42
+ equals: jar => new EqualityAssertion({ jar }),
43
+ urlEquals: baseUrl => {
47
44
  const assert = new EqualityAssertion({ jar: 'url of current page' })
48
45
  assert.comparator = function (expected, actual) {
49
46
  if (expected.indexOf('http') !== 0) {
@@ -53,5 +50,5 @@ module.exports = {
53
50
  }
54
51
  return assert
55
52
  },
56
- fileEquals: (file) => new EqualityAssertion({ file, jar: 'contents of {{file}}' }),
53
+ fileEquals: file => new EqualityAssertion({ file, jar: 'contents of {{file}}' }),
57
54
  }