codeceptjs 3.5.15 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }
@@ -1,25 +1,25 @@
1
1
  const Helper = require('@codeceptjs/helper');
2
- const AiAssistant = require('../ai');
2
+ const ai = require('../ai');
3
3
  const standardActingHelpers = require('../plugin/standardActingHelpers');
4
4
  const Container = require('../container');
5
5
  const { splitByChunks, minifyHtml } = require('../html');
6
6
 
7
7
  /**
8
- * OpenAI Helper for CodeceptJS.
8
+ * AI Helper for CodeceptJS.
9
9
  *
10
- * This helper class provides integration with the OpenAI 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.
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
11
  * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available.
12
12
  *
13
13
  * ## Configuration
14
14
  *
15
15
  * This helper should be configured in codecept.json or codecept.conf.js
16
16
  *
17
- * * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the OpenAI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
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
18
  */
19
- class OpenAI extends Helper {
19
+ class AI extends Helper {
20
20
  constructor(config) {
21
21
  super(config);
22
- this.aiAssistant = new AiAssistant();
22
+ this.aiAssistant = ai;
23
23
 
24
24
  this.options = {
25
25
  chunkSize: 80000,
@@ -39,7 +39,7 @@ class OpenAI extends Helper {
39
39
  }
40
40
 
41
41
  /**
42
- * Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML.
42
+ * Asks the AI GPT language model a question based on the provided prompt within the context of the current page's HTML.
43
43
  *
44
44
  * ```js
45
45
  * I.askGptOnPage('what does this page do?');
@@ -77,7 +77,7 @@ class OpenAI extends Helper {
77
77
  }
78
78
 
79
79
  /**
80
- * Asks the OpenAI GPT-3.5 language model a question based on the provided prompt within the context of a specific HTML fragment on the current page.
80
+ * Asks the AI a question based on the provided prompt within the context of a specific HTML fragment on the current page.
81
81
  *
82
82
  * ```js
83
83
  * I.askGptOnPageFragment('describe features of this screen', '.screen');
@@ -113,9 +113,7 @@ class OpenAI extends Helper {
113
113
  { role: 'user', content: prompt },
114
114
  ];
115
115
 
116
- const completion = await this.aiAssistant.createCompletion(messages);
117
-
118
- const response = completion?.data?.choices[0]?.message?.content;
116
+ const response = await this.aiAssistant.createCompletion(messages);
119
117
 
120
118
  console.log(response);
121
119
 
@@ -123,4 +121,4 @@ class OpenAI extends Helper {
123
121
  }
124
122
  }
125
123
 
126
- module.exports = OpenAI;
124
+ module.exports = AI;
@@ -33,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
33
33
  const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
34
34
  const Popup = require('./extras/Popup');
35
35
  const Console = require('./extras/Console');
36
- const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
36
+ const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator');
37
37
 
38
38
  let playwright;
39
39
  let perfTiming;
@@ -50,8 +50,9 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
50
50
  const {
51
51
  seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError,
52
52
  } = require('./errors/ElementAssertion');
53
- const { createAdvancedTestResults, allParameterValuePairsMatchExtreme, extractQueryObjects } = require('./networkTraffics/utils');
54
- const { log } = require('../output');
53
+ const {
54
+ dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
55
+ } = require('./network/actions');
55
56
 
56
57
  const pathSeparator = path.sep;
57
58
 
@@ -100,6 +101,7 @@ const pathSeparator = path.sep;
100
101
  * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
101
102
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
102
103
  * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
104
+ * @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
103
105
  */
104
106
  const config = {};
105
107
 
@@ -379,6 +381,7 @@ class Playwright extends Helper {
379
381
  highlightElement: false,
380
382
  };
381
383
 
384
+ process.env.testIdAttribute = 'data-testid';
382
385
  config = Object.assign(defaults, config);
383
386
 
384
387
  if (availableBrowsers.indexOf(config.browser) < 0) {
@@ -464,6 +467,7 @@ class Playwright extends Helper {
464
467
  try {
465
468
  await playwright.selectors.register('__value', createValueEngine);
466
469
  await playwright.selectors.register('__disabled', createDisabledEngine);
470
+ if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute);
467
471
  } catch (e) {
468
472
  console.warn(e);
469
473
  }
@@ -2468,7 +2472,7 @@ class Playwright extends Helper {
2468
2472
  async waitNumberOfVisibleElements(locator, num, sec) {
2469
2473
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2470
2474
  locator = new Locator(locator, 'css');
2471
- await this.context;
2475
+
2472
2476
  let waiter;
2473
2477
  const context = await this._getContext();
2474
2478
  if (locator.isCSS()) {
@@ -2957,7 +2961,7 @@ class Playwright extends Helper {
2957
2961
  * This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
2958
2962
  *
2959
2963
  * @param {string|RegExp} [url] URL, regex or pattern for to match URL
2960
- * @param {function} [handler] a function to process reques
2964
+ * @param {function} [handler] a function to process request
2961
2965
  */
2962
2966
  async mockRoute(url, handler) {
2963
2967
  return this.browserContext.route(...arguments);
@@ -2973,7 +2977,7 @@ class Playwright extends Helper {
2973
2977
  * If no handler is passed, all mock requests for the rote are disabled.
2974
2978
  *
2975
2979
  * @param {string|RegExp} [url] URL, regex or pattern for to match URL
2976
- * @param {function} [handler] a function to process reques
2980
+ * @param {function} [handler] a function to process request
2977
2981
  */
2978
2982
  async stopMockingRoute(url, handler) {
2979
2983
  return this.browserContext.unroute(...arguments);
@@ -3007,37 +3011,6 @@ class Playwright extends Helper {
3007
3011
  });
3008
3012
  }
3009
3013
 
3010
- /**
3011
- * {{> grabRecordedNetworkTraffics }}
3012
- */
3013
- async grabRecordedNetworkTraffics() {
3014
- if (!this.recording || !this.recordedAtLeastOnce) {
3015
- throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
3016
- }
3017
-
3018
- const promises = this.requests.map(async (request) => {
3019
- const resp = await request.response;
3020
- let body;
3021
- try {
3022
- // There's no 'body' for some requests (redirect etc...)
3023
- body = JSON.parse((await resp.body()).toString());
3024
- } catch (e) {
3025
- // only interested in JSON, not HTML responses.
3026
- }
3027
-
3028
- return {
3029
- url: resp.url(),
3030
- response: {
3031
- status: resp.status(),
3032
- statusText: resp.statusText(),
3033
- body,
3034
- },
3035
- };
3036
- });
3037
-
3038
- return Promise.all(promises);
3039
- }
3040
-
3041
3014
  /**
3042
3015
  * Blocks traffic of a given URL or a list of URLs.
3043
3016
  *
@@ -3117,67 +3090,19 @@ class Playwright extends Helper {
3117
3090
  }
3118
3091
 
3119
3092
  /**
3093
+ *
3120
3094
  * {{> flushNetworkTraffics }}
3121
3095
  */
3122
3096
  flushNetworkTraffics() {
3123
- this.requests = [];
3097
+ flushNetworkTraffics.call(this);
3124
3098
  }
3125
3099
 
3126
3100
  /**
3101
+ *
3127
3102
  * {{> stopRecordingTraffic }}
3128
3103
  */
3129
3104
  stopRecordingTraffic() {
3130
- this.page.removeAllListeners('request');
3131
- this.recording = false;
3132
- }
3133
-
3134
- /**
3135
- * {{> seeTraffic }}
3136
- */
3137
- async seeTraffic({
3138
- name, url, parameters, requestPostData, timeout = 10,
3139
- }) {
3140
- if (!name) {
3141
- throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
3142
- }
3143
-
3144
- if (!url) {
3145
- throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
3146
- }
3147
-
3148
- if (!this.recording || !this.recordedAtLeastOnce) {
3149
- throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
3150
- }
3151
-
3152
- for (let i = 0; i <= timeout * 2; i++) {
3153
- const found = this._isInTraffic(url, parameters);
3154
- if (found) {
3155
- return true;
3156
- }
3157
- await new Promise((done) => {
3158
- setTimeout(done, 1000);
3159
- });
3160
- }
3161
-
3162
- // check request post data
3163
- if (requestPostData && this._isInTraffic(url)) {
3164
- const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
3165
-
3166
- assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
3167
- } else if (parameters && this._isInTraffic(url)) {
3168
- const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
3169
-
3170
- assert.fail(
3171
- `Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
3172
- + `${advancedTestResults}`,
3173
- );
3174
- } else {
3175
- assert.fail(
3176
- `Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
3177
- + `Expected url: ${url}.\n`
3178
- + `Recorded traffic:\n${this._getTrafficDump()}`,
3179
- );
3180
- }
3105
+ stopRecordingTraffic.call(this);
3181
3106
  }
3182
3107
 
3183
3108
  /**
@@ -3214,83 +3139,34 @@ class Playwright extends Helper {
3214
3139
  }
3215
3140
 
3216
3141
  /**
3217
- * {{> dontSeeTraffic }}
3218
3142
  *
3143
+ * {{> grabRecordedNetworkTraffics }}
3219
3144
  */
3220
- dontSeeTraffic({ name, url }) {
3221
- if (!this.recordedAtLeastOnce) {
3222
- throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
3223
- }
3224
-
3225
- if (!name) {
3226
- throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
3227
- }
3228
-
3229
- if (!url) {
3230
- throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
3231
- }
3232
-
3233
- if (this._isInTraffic(url)) {
3234
- assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
3235
- }
3145
+ async grabRecordedNetworkTraffics() {
3146
+ return grabRecordedNetworkTraffics.call(this);
3236
3147
  }
3237
3148
 
3238
3149
  /**
3239
- * Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
3240
3150
  *
3241
- * @param url URL to look for.
3242
- * @param [parameters] Parameters that this URL needs to contain
3243
- * @return {boolean} Whether or not URL with parameters is part of network traffic.
3244
- * @private
3151
+ * {{> seeTraffic }}
3245
3152
  */
3246
- _isInTraffic(url, parameters) {
3247
- let isInTraffic = false;
3248
- this.requests.forEach((request) => {
3249
- if (isInTraffic) {
3250
- return; // We already found traffic. Continue with next request
3251
- }
3252
-
3253
- if (!request.url.match(new RegExp(url))) {
3254
- return; // url not found in this request. continue with next request
3255
- }
3256
-
3257
- // URL has matched. Now we check the parameters
3258
-
3259
- if (parameters) {
3260
- const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
3261
- if (advancedReport === true) {
3262
- isInTraffic = true;
3263
- }
3264
- } else {
3265
- isInTraffic = true;
3266
- }
3267
- });
3268
-
3269
- return isInTraffic;
3153
+ async seeTraffic({
3154
+ name, url, parameters, requestPostData, timeout = 10,
3155
+ }) {
3156
+ await seeTraffic.call(this, ...arguments);
3270
3157
  }
3271
3158
 
3272
3159
  /**
3273
- * Returns all URLs of all network requests recorded so far during execution of test scenario.
3274
3160
  *
3275
- * @return {string} List of URLs recorded as a string, separated by new lines after each URL
3276
- * @private
3161
+ * {{> dontSeeTraffic }}
3162
+ *
3277
3163
  */
3278
- _getTrafficDump() {
3279
- let dumpedTraffic = '';
3280
- this.requests.forEach((request) => {
3281
- dumpedTraffic += `${request.method} - ${request.url}\n`;
3282
- });
3283
- return dumpedTraffic;
3164
+ dontSeeTraffic({ name, url }) {
3165
+ dontSeeTraffic.call(this, ...arguments);
3284
3166
  }
3285
3167
 
3286
3168
  /**
3287
- * Starts recording of websocket messages.
3288
- * This also resets recorded websocket messages.
3289
- *
3290
- * ```js
3291
- * await I.startRecordingWebSocketMessages();
3292
- * ```
3293
- *
3169
+ * {{> startRecordingWebSocketMessages }}
3294
3170
  */
3295
3171
  async startRecordingWebSocketMessages() {
3296
3172
  this.flushWebSocketMessages();
@@ -3324,11 +3200,7 @@ class Playwright extends Helper {
3324
3200
  }
3325
3201
 
3326
3202
  /**
3327
- * Stops recording WS messages. Recorded WS messages is not flashed.
3328
- *
3329
- * ```js
3330
- * await I.stopRecordingWebSocketMessages();
3331
- * ```
3203
+ * {{> stopRecordingWebSocketMessages }}
3332
3204
  */
3333
3205
  async stopRecordingWebSocketMessages() {
3334
3206
  await this.cdpSession.send('Network.disable');
@@ -3455,6 +3327,7 @@ function buildLocatorString(locator) {
3455
3327
  async function findElements(matcher, locator) {
3456
3328
  if (locator.react) return findReact(matcher, locator);
3457
3329
  if (locator.vue) return findVue(matcher, locator);
3330
+ if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
3458
3331
  locator = new Locator(locator, 'css');
3459
3332
 
3460
3333
  return matcher.locator(buildLocatorString(locator)).all();
@@ -3462,6 +3335,8 @@ async function findElements(matcher, locator) {
3462
3335
 
3463
3336
  async function findElement(matcher, locator) {
3464
3337
  if (locator.react) return findReact(matcher, locator);
3338
+ if (locator.vue) return findVue(matcher, locator);
3339
+ if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
3465
3340
  locator = new Locator(locator, 'css');
3466
3341
 
3467
3342
  return matcher.locator(buildLocatorString(locator)).first();
@@ -3517,6 +3392,7 @@ async function proceedClick(locator, context = null, options = {}) {
3517
3392
  async function findClickable(matcher, locator) {
3518
3393
  if (locator.react) return findReact(matcher, locator);
3519
3394
  if (locator.vue) return findVue(matcher, locator);
3395
+ if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
3520
3396
 
3521
3397
  locator = new Locator(locator);
3522
3398
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);