codeceptjs 3.5.9 → 3.5.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.
@@ -1216,6 +1216,21 @@ let inputs = await I.grabValueFromAll('//form/input');
1216
1216
 
1217
1217
  Returns **[Promise][13]<[Array][15]<[string][6]>>** attribute value
1218
1218
 
1219
+ ### grabWebElements
1220
+
1221
+ Grab WebElements for given locator
1222
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
1223
+
1224
+ ```js
1225
+ const webElements = await I.grabWebElements('#button');
1226
+ ```
1227
+
1228
+ #### Parameters
1229
+
1230
+ - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator.
1231
+
1232
+ Returns **[Promise][13]<any>** WebElement of being used Web helper
1233
+
1219
1234
  ### handleDownloads
1220
1235
 
1221
1236
  Sets a directory to where save files. Allows to test file downloads.
@@ -1367,6 +1367,21 @@ let inputs = await I.grabValueFromAll('//form/input');
1367
1367
 
1368
1368
  Returns **[Promise][25]<[Array][28]<[string][17]>>** attribute value
1369
1369
 
1370
+ ### grabWebElements
1371
+
1372
+ Grab WebElements for given locator
1373
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
1374
+
1375
+ ```js
1376
+ const webElements = await I.grabWebElements('#button');
1377
+ ```
1378
+
1379
+ #### Parameters
1380
+
1381
+ - `locator` **([string][17] | [object][16])** element located by CSS|XPath|strict locator.
1382
+
1383
+ Returns **[Promise][25]<any>** WebElement of being used Web helper
1384
+
1370
1385
  ### moveCursorTo
1371
1386
 
1372
1387
  Moves cursor to element matched by locator.
@@ -119,6 +119,7 @@ Available events:
119
119
  * `event.all.result` - when results are printed
120
120
  * `event.workers.before` - before spawning workers in parallel run
121
121
  * `event.workers.after` - after workers finished in parallel run
122
+ * `event.workers.result` - test results after workers finished in parallel run
122
123
 
123
124
 
124
125
  > *sync* - means that event is fired in the moment of the action happening.
package/docs/parallel.md CHANGED
@@ -26,12 +26,124 @@ This command is similar to `run`, however, steps output can't be shown in worker
26
26
 
27
27
  Each worker spins an instance of CodeceptJS, executes a group of tests, and sends back report to the main process.
28
28
 
29
- By default the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to assigne the suites one by one to the workers.
29
+ By default, the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to assign the suites one by one to the workers.
30
30
 
31
31
  ```sh
32
32
  npx codeceptjs run-workers --suites 2
33
33
  ```
34
34
 
35
+ ## Test stats with Parallel Execution by Workers
36
+
37
+ ```js
38
+ const { event } = require('codeceptjs');
39
+
40
+ module.exports = function() {
41
+
42
+ event.dispatcher.on(event.workers.result, function (result) {
43
+
44
+ console.log(result);
45
+
46
+ });
47
+ }
48
+
49
+ // in console log
50
+ FAIL | 7 passed, 1 failed, 1 skipped // 2s
51
+ {
52
+ "tests": {
53
+ "passed": [
54
+ {
55
+ "type": "test",
56
+ "title": "Assert @C3",
57
+ "body": "() => { }",
58
+ "async": 0,
59
+ "sync": true,
60
+ "_timeout": 2000,
61
+ "_slow": 75,
62
+ "_retries": -1,
63
+ "timedOut": false,
64
+ "_currentRetry": 0,
65
+ "pending": false,
66
+ "opts": {},
67
+ "tags": [
68
+ "@C3"
69
+ ],
70
+ "uid": "xe4q1HdqpRrZG5dPe0JG+A",
71
+ "workerIndex": 3,
72
+ "retries": -1,
73
+ "duration": 493,
74
+ "err": null,
75
+ "parent": {
76
+ "title": "My",
77
+ "ctx": {},
78
+ "suites": [],
79
+ "tests": [],
80
+ "root": false,
81
+ "pending": false,
82
+ "_retries": -1,
83
+ "_beforeEach": [],
84
+ "_beforeAll": [],
85
+ "_afterEach": [],
86
+ "_afterAll": [],
87
+ "_timeout": 2000,
88
+ "_slow": 75,
89
+ "_bail": false,
90
+ "_onlyTests": [],
91
+ "_onlySuites": [],
92
+ "delayed": false
93
+ },
94
+ "steps": [
95
+ {
96
+ "actor": "I",
97
+ "name": "amOnPage",
98
+ "status": "success",
99
+ "agrs": [
100
+ "https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"
101
+ ],
102
+ "startedAt": 1698760652610,
103
+ "startTime": 1698760652611,
104
+ "endTime": 1698760653098,
105
+ "finishedAt": 1698760653098,
106
+ "duration": 488
107
+ },
108
+ {
109
+ "actor": "I",
110
+ "name": "grabCurrentUrl",
111
+ "status": "success",
112
+ "agrs": [],
113
+ "startedAt": 1698760653098,
114
+ "startTime": 1698760653098,
115
+ "endTime": 1698760653099,
116
+ "finishedAt": 1698760653099,
117
+ "duration": 1
118
+ }
119
+ ]
120
+ }
121
+ ],
122
+ "failed": [],
123
+ "skipped": []
124
+ }
125
+ }
126
+ ```
127
+
128
+ CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with `run-workers` command so that you could handle the events better in your plugins/helpers
129
+
130
+ ```js
131
+ const { event } = require('codeceptjs');
132
+
133
+ module.exports = function() {
134
+ // this event would trigger the `_publishResultsToTestrail` when running `run-workers` command
135
+ event.dispatcher.on(event.workers.result, async () => {
136
+ await _publishResultsToTestrail();
137
+ });
138
+
139
+ // this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command
140
+ event.dispatcher.on(event.all.result, async () => {
141
+ // when running `run` command, this env var is undefined
142
+ if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail();
143
+ });
144
+ }
145
+ ```
146
+
35
147
  ## Parallel Execution by Workers on Multiple Browsers
36
148
 
37
149
  To run tests in parallel across multiple browsers, modify your `codecept.conf.js` file to configure multiple browsers on which you want to run your tests and your tests will run across multiple browsers.
@@ -236,7 +348,7 @@ customWorkers.on(event.all.result, () => {
236
348
 
237
349
  ### Emitting messages to the parent worker
238
350
 
239
- Child workers can send non test events to the main process. This is useful if you want to pass along information not related to the tests event cycles itself such as `event.test.success`.
351
+ Child workers can send non-test events to the main process. This is useful if you want to pass along information not related to the tests event cycles itself such as `event.test.success`.
240
352
 
241
353
  ```js
242
354
  // inside main process
package/docs/plugins.md CHANGED
@@ -65,12 +65,14 @@ If a session expires automatically logs in again.
65
65
  ```js
66
66
  // inside a test file
67
67
  // use login to inject auto-login function
68
+ Feature('Login');
69
+
68
70
  Before(({ login }) => {
69
71
  login('user'); // login using user session
70
72
  });
71
73
 
72
- // Alternatively log in for one scenario
73
- Scenario('log me in', ( {I, login} ) => {
74
+ // Alternatively log in for one scenario.
75
+ Scenario('log me in', ( { I, login } ) => {
74
76
  login('admin');
75
77
  I.see('I am logged in');
76
78
  });
@@ -0,0 +1,9 @@
1
+ Grab WebElement for given locator
2
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
3
+
4
+ ```js
5
+ const webElement = await I.grabWebElement('#button');
6
+ ```
7
+
8
+ @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
9
+ @returns {Promise<*>} WebElement of being used Web helper
@@ -0,0 +1,9 @@
1
+ Grab WebElements for given locator
2
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
3
+
4
+ ```js
5
+ const webElements = await I.grabWebElements('#button');
6
+ ```
7
+
8
+ @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
9
+ @returns {Promise<*>} WebElement of being used Web helper
package/lib/ai.js CHANGED
@@ -16,6 +16,8 @@ const htmlConfig = {
16
16
  html: {},
17
17
  };
18
18
 
19
+ const aiInstance = null;
20
+
19
21
  class AiAssistant {
20
22
  constructor() {
21
23
  this.config = config.get('ai', defaultConfig);
@@ -26,7 +28,10 @@ class AiAssistant {
26
28
 
27
29
  this.isEnabled = !!process.env.OPENAI_API_KEY;
28
30
 
29
- if (!this.isEnabled) return;
31
+ if (!this.isEnabled) {
32
+ debug('No OpenAI API key provided. AI assistant is disabled.');
33
+ return;
34
+ }
30
35
 
31
36
  const configuration = new Configuration({
32
37
  apiKey: process.env.OPENAI_API_KEY,
@@ -35,13 +40,17 @@ class AiAssistant {
35
40
  this.openai = new OpenAIApi(configuration);
36
41
  }
37
42
 
38
- setHtmlContext(html) {
43
+ static getInstance() {
44
+ return aiInstance || new AiAssistant();
45
+ }
46
+
47
+ async setHtmlContext(html) {
39
48
  let processedHTML = html;
40
49
 
41
50
  if (this.htmlConfig.simplify) {
42
51
  processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
43
52
  }
44
- if (this.htmlConfig.minify) processedHTML = minifyHtml(processedHTML);
53
+ if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
45
54
  if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
46
55
 
47
56
  debug(processedHTML);
package/lib/colorUtils.js CHANGED
@@ -226,15 +226,25 @@ function isColorProperty(prop) {
226
226
  'color',
227
227
  'background',
228
228
  'backgroundColor',
229
+ 'background-color',
229
230
  'borderColor',
231
+ 'border-color',
230
232
  'borderBottomColor',
233
+ 'border-bottom-color',
231
234
  'borderLeftColor',
235
+ 'border-left-color',
232
236
  'borderRightColor',
233
237
  'borderTopColor',
234
238
  'caretColor',
235
239
  'columnRuleColor',
236
240
  'outlineColor',
237
241
  'textDecorationColor',
242
+ 'border-right-color',
243
+ 'border-top-color',
244
+ 'caret-color',
245
+ 'column-rule-color',
246
+ 'outline-color',
247
+ 'text-decoration-color',
238
248
  ].indexOf(prop) > -1;
239
249
  }
240
250
 
@@ -124,7 +124,7 @@ function executeRun(runName, runConfig) {
124
124
  if (browserConfig.outputName) {
125
125
  outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
126
126
  } else {
127
- const hash = crypto.createHash('md5');
127
+ const hash = crypto.createHash('sha256');
128
128
  hash.update(JSON.stringify(browserConfig));
129
129
  outputDir += hash.digest('hex');
130
130
  }
@@ -7,6 +7,11 @@ const Workers = require('../workers');
7
7
  module.exports = async function (workerCount, selectedRuns, options) {
8
8
  process.env.profile = options.profile;
9
9
 
10
+ const suiteArr = [];
11
+ const passedTestArr = [];
12
+ const failedTestArr = [];
13
+ const skippedTestArr = [];
14
+
10
15
  const { config: testConfig, override = '' } = options;
11
16
  const overrideConfigs = tryOrDefault(() => JSON.parse(override), {});
12
17
  const by = options.suites ? 'suite' : 'test';
@@ -26,15 +31,36 @@ module.exports = async function (workerCount, selectedRuns, options) {
26
31
 
27
32
  const workers = new Workers(numberOfWorkers, config);
28
33
  workers.overrideConfig(overrideConfigs);
29
- workers.on(event.test.failed, (failedTest) => {
30
- output.test.failed(failedTest);
34
+
35
+ workers.on(event.suite.before, (suite) => {
36
+ suiteArr.push(suite);
37
+ });
38
+
39
+ workers.on(event.test.failed, (test) => {
40
+ failedTestArr.push(test);
41
+ output.test.failed(test);
42
+ });
43
+
44
+ workers.on(event.test.passed, (test) => {
45
+ passedTestArr.push(test);
46
+ output.test.passed(test);
31
47
  });
32
48
 
33
- workers.on(event.test.passed, (successTest) => {
34
- output.test.passed(successTest);
49
+ workers.on(event.test.skipped, (test) => {
50
+ skippedTestArr.push(test);
51
+ output.test.passed(test);
35
52
  });
36
53
 
37
54
  workers.on(event.all.result, () => {
55
+ // expose test stats after all workers finished their execution
56
+ event.dispatcher.emit(event.workers.result, {
57
+ suites: suiteArr,
58
+ tests: {
59
+ passed: passedTestArr,
60
+ failed: failedTestArr,
61
+ skipped: skippedTestArr,
62
+ },
63
+ });
38
64
  workers.printResults();
39
65
  });
40
66
 
@@ -132,9 +132,32 @@ function initializeListeners() {
132
132
  duration: test.duration || 0,
133
133
  err,
134
134
  parent,
135
+ steps: test.steps ? simplifyStepsInTestObject(test.steps, err) : [],
135
136
  };
136
137
  }
137
138
 
139
+ function simplifyStepsInTestObject(steps, err) {
140
+ steps = [...steps];
141
+ const _steps = [];
142
+
143
+ for (step of steps) {
144
+ _steps.push({
145
+ actor: step.actor,
146
+ name: step.name,
147
+ status: step.status,
148
+ agrs: step.args,
149
+ startedAt: step.startedAt,
150
+ startTime: step.startTime,
151
+ endTime: step.endTime,
152
+ finishedAt: step.finishedAt,
153
+ duration: step.duration,
154
+ err,
155
+ });
156
+ }
157
+
158
+ return _steps;
159
+ }
160
+
138
161
  function simplifyStep(step, err = null) {
139
162
  step = { ...step };
140
163
 
package/lib/event.js CHANGED
@@ -127,10 +127,12 @@ module.exports = {
127
127
  * @inner
128
128
  * @property {'workers.before'} before
129
129
  * @property {'workers.after'} after
130
+ * @property {'workers.result'} result
130
131
  */
131
132
  workers: {
132
133
  before: 'workers.before',
133
134
  after: 'workers.after',
135
+ result: 'workers.result',
134
136
  },
135
137
 
136
138
  /**
@@ -276,7 +276,7 @@ class Appium extends Webdriver {
276
276
  const _convertedCaps = {};
277
277
  for (const [key, value] of Object.entries(capabilities)) {
278
278
  if (!key.startsWith(vendorPrefix.appium)) {
279
- if (key !== 'platformName') {
279
+ if (key !== 'platformName' && key !== 'bstack:options') {
280
280
  _convertedCaps[`${vendorPrefix.appium}:${key}`] = value;
281
281
  } else {
282
282
  _convertedCaps[`${key}`] = value;
@@ -368,7 +368,7 @@ class Appium extends Webdriver {
368
368
  if (context.web) return this.switchToWeb(context.web);
369
369
  if (context.webview) return this.switchToWeb(context.webview);
370
370
  }
371
- return this._switchToContext(context);
371
+ return this.switchToContext(context);
372
372
  }
373
373
 
374
374
  _withinEnd() {
@@ -423,8 +423,8 @@ class Appium extends Webdriver {
423
423
  async runOnIOS(caps, fn) {
424
424
  if (this.platform !== 'ios') return;
425
425
  recorder.session.start('iOS-only actions');
426
- this._runWithCaps(caps, fn);
427
- recorder.add('restore from iOS session', () => recorder.session.restore());
426
+ await this._runWithCaps(caps, fn);
427
+ await recorder.add('restore from iOS session', () => recorder.session.restore());
428
428
  return recorder.promise();
429
429
  }
430
430
 
@@ -465,8 +465,8 @@ class Appium extends Webdriver {
465
465
  async runOnAndroid(caps, fn) {
466
466
  if (this.platform !== 'android') return;
467
467
  recorder.session.start('Android-only actions');
468
- this._runWithCaps(caps, fn);
469
- recorder.add('restore from Android session', () => recorder.session.restore());
468
+ await this._runWithCaps(caps, fn);
469
+ await recorder.add('restore from Android session', () => recorder.session.restore());
470
470
  return recorder.promise();
471
471
  }
472
472
 
@@ -834,7 +834,7 @@ class Appium extends Webdriver {
834
834
  *
835
835
  * @param {*} context the context to switch to
836
836
  */
837
- async _switchToContext(context) {
837
+ async switchToContext(context) {
838
838
  return this.browser.switchContext(context);
839
839
  }
840
840
 
@@ -858,11 +858,11 @@ class Appium extends Webdriver {
858
858
  this.isWeb = true;
859
859
  this.defaultContext = 'body';
860
860
 
861
- if (context) return this._switchToContext(context);
861
+ if (context) return this.switchToContext(context);
862
862
  const contexts = await this.grabAllContexts();
863
863
  this.debugSection('Contexts', contexts.toString());
864
864
  for (const idx in contexts) {
865
- if (contexts[idx].match(/^WEBVIEW/)) return this._switchToContext(contexts[idx]);
865
+ if (contexts[idx].match(/^WEBVIEW/)) return this.switchToContext(contexts[idx]);
866
866
  }
867
867
 
868
868
  throw new Error('No WEBVIEW could be guessed, please specify one in params');
@@ -885,8 +885,8 @@ class Appium extends Webdriver {
885
885
  this.isWeb = false;
886
886
  this.defaultContext = '//*';
887
887
 
888
- if (context) return this._switchToContext(context);
889
- return this._switchToContext('NATIVE_APP');
888
+ if (context) return this.switchToContext(context);
889
+ return this.switchToContext('NATIVE_APP');
890
890
  }
891
891
 
892
892
  /**
@@ -1424,10 +1424,10 @@ class Appium extends Webdriver {
1424
1424
  *
1425
1425
  * @return {Promise<void>}
1426
1426
  *
1427
- * Appium: support only iOS
1427
+ * Appium: support both Android and iOS
1428
1428
  */
1429
1429
  async closeApp() {
1430
- onlyForApps.call(this, 'iOS');
1430
+ onlyForApps.call(this);
1431
1431
  return this.browser.closeApp();
1432
1432
  }
1433
1433