codeceptjs 3.5.0 → 3.5.1

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/docs/plugins.md CHANGED
@@ -612,7 +612,7 @@ Self-healing tests with OpenAI.
612
612
 
613
613
  This plugin is experimental and requires OpenAI API key.
614
614
 
615
- To use it you need to set OPENAI_API_KEY env variable and enable plugin inside condig.
615
+ To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
616
616
 
617
617
  ```js
618
618
  plugins: {
@@ -0,0 +1,17 @@
1
+ Remove focus from a text input, button, etc.
2
+ Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.
3
+
4
+ Examples:
5
+
6
+ ```js
7
+ I.blur('.text-area')
8
+ ```
9
+ ```js
10
+ //element `#product-tile` is focused
11
+ I.see('#add-to-cart-btn');
12
+ I.blur('#product-tile')
13
+ I.dontSee('#add-to-cart-btn');
14
+ ```
15
+
16
+ @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
17
+ @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
@@ -0,0 +1,12 @@
1
+ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
2
+
3
+ Examples:
4
+
5
+ ```js
6
+ I.dontSee('#add-to-cart-btn');
7
+ I.focus('#product-tile')
8
+ I.see('#add-to-cart-bnt');
9
+ ```
10
+
11
+ @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
12
+ @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
package/lib/cli.js CHANGED
@@ -81,6 +81,11 @@ class Cli extends Base {
81
81
  if (!codeceptjsEventDispatchersRegistered) {
82
82
  codeceptjsEventDispatchersRegistered = true;
83
83
 
84
+ event.dispatcher.on(event.bddStep.started, (step) => {
85
+ output.stepShift = 2;
86
+ output.step(step);
87
+ });
88
+
84
89
  event.dispatcher.on(event.step.started, (step) => {
85
90
  let processingStep = step;
86
91
  const metaSteps = [];
@@ -93,7 +98,10 @@ class Cli extends Base {
93
98
  for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
94
99
  if (currentMetaStep[i] !== metaSteps[i]) {
95
100
  output.stepShift = 3 + 2 * i;
96
- if (metaSteps[i]) output.step(metaSteps[i]);
101
+ if (!metaSteps[i]) continue;
102
+ // bdd steps are handled by bddStep.started
103
+ if (metaSteps[i].isBDD()) continue;
104
+ output.step(metaSteps[i]);
97
105
  }
98
106
  }
99
107
  currentMetaStep = metaSteps;
@@ -317,6 +317,16 @@ module.exports = function (initPath) {
317
317
 
318
318
  print('Configure helpers...');
319
319
  inquirer.prompt(helperConfigs).then((helperResult) => {
320
+ if (helperResult.Playwright_browser === 'electron') {
321
+ delete helperResult.Playwright_url;
322
+ delete helperResult.Playwright_show;
323
+
324
+ helperResult.Playwright_electron = {
325
+ executablePath: '// require("electron") or require("electron-forge")',
326
+ args: ['path/to/your/main.js'],
327
+ };
328
+ }
329
+
320
330
  Object.keys(helperResult).forEach((key) => {
321
331
  const parts = key.split('_');
322
332
  const helperName = parts[0];
package/lib/event.js CHANGED
@@ -91,6 +91,8 @@ module.exports = {
91
91
  bddStep: {
92
92
  before: 'bddStep.before',
93
93
  after: 'bddStep.after',
94
+ started: 'bddStep.started',
95
+ finished: 'bddStep.finished',
94
96
  },
95
97
  /**
96
98
  * @type {object}
@@ -52,13 +52,13 @@ const pathSeparator = path.sep;
52
52
  /**
53
53
  * ## Configuration
54
54
  *
55
- * This helper should be configured in codecept.conf.js
55
+ * This helper should be configured in codecept.conf.(js|ts)
56
56
  *
57
57
  * @typedef PlaywrightConfig
58
58
  * @type {object}
59
- * @prop {string} url - base url of website to be tested
59
+ * @prop {string} [url] - base url of website to be tested
60
60
  * @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
61
- * @prop {boolean} [show=false] - show browser window.
61
+ * @prop {boolean} [show=true] - show browser window.
62
62
  * @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
63
63
  * * 'context' or **false** - restarts [browser context](https://playwright.dev/docs/api/class-browsercontext) but keeps running browser. Recommended by Playwright team to keep tests isolated.
64
64
  * * 'browser' or **true** - closes browser and opens it again between tests.
@@ -260,6 +260,22 @@ const config = {};
260
260
  * }
261
261
  * ```
262
262
  *
263
+ * * #### Example #9: Launch electron test
264
+ *
265
+ * ```js
266
+ * {
267
+ * helpers: {
268
+ * Playwright: {
269
+ * browser: 'electron',
270
+ * electron: {
271
+ * executablePath: require("electron"),
272
+ * args: [path.join('../', "main.js")],
273
+ * },
274
+ * }
275
+ * },
276
+ * }
277
+ * ```
278
+ *
263
279
  * Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
264
280
  *
265
281
  * ## Access From Helpers
@@ -373,15 +389,24 @@ class Playwright extends Helper {
373
389
 
374
390
  static _config() {
375
391
  return [
376
- { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
377
- {
378
- name: 'show', message: 'Show browser window', default: true, type: 'confirm',
379
- },
380
392
  {
381
393
  name: 'browser',
382
394
  message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
383
395
  default: 'chromium',
384
396
  },
397
+ {
398
+ name: 'url',
399
+ message: 'Base url of site to be tested',
400
+ default: 'http://localhost',
401
+ when: (answers) => answers.Playwright_browser !== 'electron',
402
+ },
403
+ {
404
+ name: 'show',
405
+ message: 'Show browser window',
406
+ default: true,
407
+ type: 'confirm',
408
+ when: (answers) => answers.Playwright_browser !== 'electron',
409
+ },
385
410
  ];
386
411
  }
387
412
 
@@ -908,17 +933,7 @@ class Playwright extends Helper {
908
933
  }
909
934
 
910
935
  /**
911
- * Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
912
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
913
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
914
- *
915
- * Examples:
916
- *
917
- * ```js
918
- * I.dontSee('#add-to-cart-btn');
919
- * I.focus('#product-tile')
920
- * I.see('#add-to-cart-bnt');
921
- * ```
936
+ * {{> focus }}
922
937
  *
923
938
  */
924
939
  async focus(locator, options = {}) {
@@ -931,22 +946,7 @@ class Playwright extends Helper {
931
946
  }
932
947
 
933
948
  /**
934
- * Remove focus from a text input, button, etc
935
- * Calls [blur](https://playwright.dev/docs/api/class-locator#locator-blur) on the element.
936
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
937
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
938
- *
939
- * Examples:
940
- *
941
- * ```js
942
- * I.blur('.text-area')
943
- * ```
944
- * ```js
945
- * //element `#product-tile` is focused
946
- * I.see('#add-to-cart-btn');
947
- * I.blur('#product-tile')
948
- * I.dontSee('#add-to-cart-btn');
949
- * ```
949
+ * {{> blur }}
950
950
  *
951
951
  */
952
952
  async blur(locator, options = {}) {
@@ -1542,18 +1542,21 @@ class Playwright extends Helper {
1542
1542
  }
1543
1543
 
1544
1544
  /**
1545
- * Clear the <input>, <textarea> or [contenteditable] .
1545
+ * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
1546
+ *
1547
+ *
1548
+ * Examples:
1549
+ *
1550
+ * ```js
1551
+ * I.clearField('.text-area')
1552
+ *
1553
+ * // if this doesn't work use force option
1554
+ * I.clearField('#submit', { force: true })
1555
+ * ```
1556
+ * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1557
+ *
1546
1558
  * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1547
1559
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1548
- *
1549
- * Examples:
1550
- *
1551
- * ```js
1552
- * I.clearField('.text-area')
1553
- * ```
1554
- * ```js
1555
- * I.clearField('#submit', { force: true }) // force to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1556
- * ```
1557
1560
  */
1558
1561
  async clearField(locator, options = {}) {
1559
1562
  let result;
@@ -35,6 +35,8 @@ const Popup = require('./extras/Popup');
35
35
  const Console = require('./extras/Console');
36
36
  const findReact = require('./extras/React');
37
37
  const { highlightElement } = require('./scripts/highlightElement');
38
+ const { blurElement } = require('./scripts/blurElement');
39
+ const { focusElement } = require('./scripts/focusElement');
38
40
 
39
41
  let puppeteer;
40
42
  let perfTiming;
@@ -707,6 +709,31 @@ class Puppeteer extends Helper {
707
709
  return this._waitForAction();
708
710
  }
709
711
 
712
+ /**
713
+ * {{> focus }}
714
+ *
715
+ */
716
+ async focus(locator) {
717
+ const els = await this._locate(locator);
718
+ assertElementExists(els, locator, 'Element to focus');
719
+ const el = els[0];
720
+
721
+ await focusElement(el, this.page);
722
+ return this._waitForAction();
723
+ }
724
+
725
+ /**
726
+ * {{> blur }}
727
+ *
728
+ */
729
+ async blur(locator) {
730
+ const els = await this._locate(locator);
731
+ assertElementExists(els, locator, 'Element to blur');
732
+
733
+ await blurElement(els[0], this.page);
734
+ return this._waitForAction();
735
+ }
736
+
710
737
  /**
711
738
  * {{> dragAndDrop }}
712
739
  */
@@ -346,6 +346,34 @@ class TestCafe extends Helper {
346
346
  return this.t.resizeWindow(width, height).catch(mapError);
347
347
  }
348
348
 
349
+ /**
350
+ * {{> focus }}
351
+ *
352
+ */
353
+ async focus(locator) {
354
+ const els = await this._locate(locator);
355
+ await assertElementExists(els, locator, 'Element to focus');
356
+ const element = await els.nth(0);
357
+
358
+ const focusElement = ClientFunction(() => element().focus(), { boundTestRun: this.t, dependencies: { element } });
359
+
360
+ return focusElement();
361
+ }
362
+
363
+ /**
364
+ * {{> blur }}
365
+ *
366
+ */
367
+ async blur(locator) {
368
+ const els = await this._locate(locator);
369
+ await assertElementExists(els, locator, 'Element to blur');
370
+ const element = await els.nth(0);
371
+
372
+ const blurElement = ClientFunction(() => element().blur(), { boundTestRun: this.t, dependencies: { element } });
373
+
374
+ return blurElement();
375
+ }
376
+
349
377
  /**
350
378
  * {{> click }}
351
379
  *
@@ -792,7 +820,7 @@ class TestCafe extends Helper {
792
820
  /**
793
821
  * {{> executeScript }}
794
822
  *
795
- * If a function returns a Promise It will wait for it resolution.
823
+ * If a function returns a Promise It will wait for its resolution.
796
824
  */
797
825
  async executeScript(fn, ...args) {
798
826
  const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
@@ -30,6 +30,8 @@ const ConnectionRefused = require('./errors/ConnectionRefused');
30
30
  const Locator = require('../locator');
31
31
  const { highlightElement } = require('./scripts/highlightElement');
32
32
  const store = require('../store');
33
+ const { focusElement } = require('./scripts/focusElement');
34
+ const { blurElement } = require('./scripts/blurElement');
33
35
 
34
36
  const SHADOW = 'shadow';
35
37
  const webRoot = 'body';
@@ -1934,6 +1936,30 @@ class WebDriver extends Helper {
1934
1936
  }
1935
1937
  }
1936
1938
 
1939
+ /**
1940
+ * {{> focus }}
1941
+ *
1942
+ */
1943
+ async focus(locator) {
1944
+ const els = await this._locate(locator);
1945
+ assertElementExists(els, locator, 'Element to focus');
1946
+ const el = usingFirstElement(els);
1947
+
1948
+ await focusElement(el, this.browser);
1949
+ }
1950
+
1951
+ /**
1952
+ * {{> blur }}
1953
+ *
1954
+ */
1955
+ async blur(locator) {
1956
+ const els = await this._locate(locator);
1957
+ assertElementExists(els, locator, 'Element to blur');
1958
+ const el = usingFirstElement(els);
1959
+
1960
+ await blurElement(el, this.browser);
1961
+ }
1962
+
1937
1963
  /**
1938
1964
  * {{> dragAndDrop }}
1939
1965
  * Appium: not tested
@@ -0,0 +1,17 @@
1
+ module.exports.blurElement = (element, context) => {
2
+ const clientSideBlurFn = el => {
3
+ el.blur();
4
+ };
5
+
6
+ try {
7
+ // Puppeteer
8
+ context.evaluate(clientSideBlurFn, element);
9
+ } catch (e) {
10
+ // WebDriver
11
+ try {
12
+ context.execute(clientSideBlurFn, element);
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ }
17
+ };
@@ -0,0 +1,17 @@
1
+ module.exports.focusElement = (element, context) => {
2
+ const clientSideFn = el => {
3
+ el.focus();
4
+ };
5
+
6
+ try {
7
+ // Puppeteer
8
+ context.evaluate(clientSideFn, element);
9
+ } catch (e) {
10
+ // WebDriver
11
+ try {
12
+ context.execute(clientSideFn, element);
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ }
17
+ };
@@ -1,6 +1,7 @@
1
1
  const Gherkin = require('@cucumber/gherkin');
2
2
  const Messages = require('@cucumber/messages');
3
3
  const { Context, Suite, Test } = require('mocha');
4
+ const debug = require('debug')('codeceptjs:bdd');
4
5
 
5
6
  const { matchStep } = require('./bdd');
6
7
  const event = require('../event');
@@ -39,7 +40,9 @@ module.exports = (text, file) => {
39
40
  for (const step of steps) {
40
41
  const metaStep = new Step.MetaStep(null, step.text);
41
42
  metaStep.actor = step.keyword.trim();
43
+ let helperStep;
42
44
  const setMetaStep = (step) => {
45
+ helperStep = step;
43
46
  if (step.metaStep) {
44
47
  if (step.metaStep === metaStep) {
45
48
  return;
@@ -67,11 +70,15 @@ module.exports = (text, file) => {
67
70
  step.startTime = Date.now();
68
71
  step.match = fn.line;
69
72
  event.emit(event.bddStep.before, step);
73
+ event.emit(event.bddStep.started, metaStep);
70
74
  event.dispatcher.prependListener(event.step.before, setMetaStep);
71
75
  try {
76
+ debug(`Step '${step.text}' started...`);
72
77
  await fn(...fn.params);
78
+ debug('Step passed');
73
79
  step.status = 'passed';
74
80
  } catch (err) {
81
+ debug(`Step failed: ${err?.message}`);
75
82
  step.status = 'failed';
76
83
  step.err = err;
77
84
  throw err;
@@ -79,6 +86,7 @@ module.exports = (text, file) => {
79
86
  step.endTime = Date.now();
80
87
  event.dispatcher.removeListener(event.step.before, setMetaStep);
81
88
  }
89
+ event.emit(event.bddStep.finished, metaStep);
82
90
  event.emit(event.bddStep.after, step);
83
91
  }
84
92
  };
@@ -26,7 +26,7 @@ const defaultConfig = {
26
26
  *
27
27
  * This plugin is experimental and requires OpenAI API key.
28
28
  *
29
- * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside condig.
29
+ * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
30
30
  *
31
31
  * ```js
32
32
  * plugins: {
@@ -82,7 +82,7 @@ module.exports = function (config = {}) {
82
82
  const test = currentTest;
83
83
 
84
84
  if (healedSteps >= config.healLimit) {
85
- output.print(colors.bold.red(`Can't heal more than ${config.healLimit} steps in a test`));
85
+ output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
86
86
  output.print('Entire flow can be broken, please check it manually');
87
87
  output.print('or increase healing limit in heal plugin config');
88
88
 
@@ -126,7 +126,7 @@ module.exports = function (config = {}) {
126
126
  print('===================');
127
127
  print(colors.bold.green('Self-Healing Report:'));
128
128
 
129
- print(`${colors.bold(healSuggestions.length)} steps were healed by AI`);
129
+ print(`${colors.bold(healSuggestions.length)} step(s) were healed by AI`);
130
130
 
131
131
  let i = 1;
132
132
  print('');
@@ -145,15 +145,13 @@ module.exports = function (config = {}) {
145
145
  });
146
146
 
147
147
  async function tryToHeal(failedStep, err) {
148
- output.debug(`Running OpenAPI to heal ${failedStep.toCode()} step`);
149
-
150
- const I = Container.support('I');
148
+ output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
151
149
 
152
150
  const codeSnippets = await aiAssistant.healFailedStep(
153
151
  failedStep, err, currentTest,
154
152
  );
155
153
 
156
- output.debug(`Received ${codeSnippets.length} proposals from OpenAI`);
154
+ output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
157
155
 
158
156
  for (const codeSnippet of codeSnippets) {
159
157
  try {
package/lib/recorder.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const debug = require('debug')('codeceptjs:recorder');
2
2
  const promiseRetry = require('promise-retry');
3
-
3
+ const { printObjectProperties } = require('./utils');
4
4
  const { log } = require('./output');
5
5
 
6
6
  const MAX_TASKS = 100;
@@ -41,6 +41,7 @@ module.exports = {
41
41
  * @inner
42
42
  */
43
43
  start() {
44
+ debug('Starting recording promises');
44
45
  running = true;
45
46
  asyncErr = null;
46
47
  errFn = null;
@@ -175,7 +176,7 @@ module.exports = {
175
176
  return;
176
177
  }
177
178
  tasks.push(taskName);
178
- if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
179
+ debug(`${currentQueue()}Queued | ${taskName}`);
179
180
 
180
181
  return promise = Promise.resolve(promise).then((res) => {
181
182
  // prefer options for non-conditional retries
@@ -224,10 +225,11 @@ module.exports = {
224
225
  * @inner
225
226
  */
226
227
  catch(customErrFn) {
228
+ debug(`${currentQueue()}Queued | catch with error handler`);
227
229
  return promise = promise.catch((err) => {
228
230
  log(`${currentQueue()}Error | ${err}`);
229
231
  if (!(err instanceof Error)) { // strange things may happen
230
- err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
232
+ err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`); // we should be prepared for them
231
233
  }
232
234
  if (customErrFn) {
233
235
  customErrFn(err);
@@ -264,8 +266,9 @@ module.exports = {
264
266
  * @param {*} err
265
267
  * @inner
266
268
  */
269
+
267
270
  throw(err) {
268
- return this.add(`throw error ${err}`, () => {
271
+ return this.add(`throw error: ${err.message}`, () => {
269
272
  throw err;
270
273
  });
271
274
  },
package/lib/utils.js CHANGED
@@ -459,3 +459,16 @@ module.exports.isNotSet = function (obj) {
459
459
  module.exports.emptyFolder = async (directoryPath) => {
460
460
  require('child_process').execSync(`rm -rf ${directoryPath}/*`);
461
461
  };
462
+
463
+ module.exports.printObjectProperties = (obj) => {
464
+ if (typeof obj !== 'object' || obj === null) {
465
+ return obj;
466
+ }
467
+
468
+ let result = '';
469
+ for (const [key, value] of Object.entries(obj)) {
470
+ result += `${key}: "${value}"; `;
471
+ }
472
+
473
+ return `{${result}}`;
474
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -81,7 +81,6 @@
81
81
  "fs-extra": "^8.1.0",
82
82
  "glob": "^6.0.1",
83
83
  "html-minifier": "^4.0.0",
84
- "i": "^0.3.7",
85
84
  "inquirer": "^6.5.2",
86
85
  "joi": "^17.6.0",
87
86
  "js-beautify": "^1.14.0",
@@ -89,9 +88,7 @@
89
88
  "lodash.merge": "^4.6.2",
90
89
  "mkdirp": "^1.0.4",
91
90
  "mocha": "^10.2.0",
92
- "mocha-junit-reporter": "^1.23.3",
93
91
  "ms": "^2.1.3",
94
- "npm": "^9.6.7",
95
92
  "openai": "^3.2.1",
96
93
  "ora-classic": "^5.4.2",
97
94
  "parse-function": "^5.6.4",
@@ -120,7 +117,7 @@
120
117
  "dtslint": "^4.1.6",
121
118
  "electron": "^12.2.3",
122
119
  "eslint": "^6.8.0",
123
- "eslint-config-airbnb-base": "^14.2.1",
120
+ "eslint-config-airbnb-base": "^15.0.0",
124
121
  "eslint-plugin-import": "^2.25.4",
125
122
  "eslint-plugin-mocha": "^6.3.0",
126
123
  "expect": "^26.6.2",
@@ -149,7 +146,7 @@
149
146
  "typescript": "^4.8.4",
150
147
  "wdio-docker-service": "^1.5.0",
151
148
  "webdriverio": "^8.3.8",
152
- "xml2js": "^0.4.23",
149
+ "xml2js": "^0.6.0",
153
150
  "xmldom": "^0.5.0",
154
151
  "xpath": "0.0.27"
155
152
  },