codeceptjs 3.4.1 → 3.5.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +9 -7
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +246 -0
  5. package/docs/build/Appium.js +47 -7
  6. package/docs/build/JSONResponse.js +4 -4
  7. package/docs/build/Nightmare.js +3 -1
  8. package/docs/build/OpenAI.js +122 -0
  9. package/docs/build/Playwright.js +193 -45
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +45 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +3 -1
  14. package/docs/build/WebDriver.js +30 -5
  15. package/docs/changelog.md +65 -0
  16. package/docs/helpers/Appium.md +152 -147
  17. package/docs/helpers/JSONResponse.md +4 -4
  18. package/docs/helpers/Nightmare.md +2 -0
  19. package/docs/helpers/OpenAI.md +70 -0
  20. package/docs/helpers/Playwright.md +194 -152
  21. package/docs/helpers/Puppeteer.md +6 -0
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +2 -0
  24. package/docs/helpers/WebDriver.md +10 -4
  25. package/docs/mobile.md +49 -2
  26. package/docs/parallel.md +56 -0
  27. package/docs/plugins.md +87 -33
  28. package/docs/secrets.md +6 -0
  29. package/docs/tutorial.md +2 -2
  30. package/docs/webapi/appendField.mustache +2 -0
  31. package/docs/webapi/type.mustache +3 -0
  32. package/lib/ai.js +171 -0
  33. package/lib/cli.js +1 -1
  34. package/lib/codecept.js +4 -0
  35. package/lib/command/dryRun.js +9 -1
  36. package/lib/command/generate.js +46 -3
  37. package/lib/command/init.js +13 -1
  38. package/lib/command/interactive.js +15 -1
  39. package/lib/command/run-workers.js +2 -1
  40. package/lib/container.js +13 -3
  41. package/lib/helper/Appium.js +45 -7
  42. package/lib/helper/JSONResponse.js +4 -4
  43. package/lib/helper/Nightmare.js +1 -1
  44. package/lib/helper/OpenAI.js +122 -0
  45. package/lib/helper/Playwright.js +190 -38
  46. package/lib/helper/Protractor.js +1 -1
  47. package/lib/helper/Puppeteer.js +40 -12
  48. package/lib/helper/REST.js +15 -5
  49. package/lib/helper/TestCafe.js +1 -1
  50. package/lib/helper/WebDriver.js +25 -5
  51. package/lib/helper/scripts/highlightElement.js +20 -0
  52. package/lib/html.js +258 -0
  53. package/lib/listener/retry.js +2 -1
  54. package/lib/pause.js +73 -17
  55. package/lib/plugin/debugErrors.js +67 -0
  56. package/lib/plugin/fakerTransform.js +4 -6
  57. package/lib/plugin/heal.js +179 -0
  58. package/lib/plugin/screenshotOnFail.js +11 -2
  59. package/lib/recorder.js +4 -4
  60. package/lib/secret.js +5 -4
  61. package/lib/step.js +6 -1
  62. package/lib/ui.js +4 -3
  63. package/lib/utils.js +4 -0
  64. package/lib/workers.js +57 -9
  65. package/package.json +25 -13
  66. package/translations/ja-JP.js +9 -9
  67. package/typings/index.d.ts +43 -9
  68. package/typings/promiseBasedTypes.d.ts +124 -24
  69. package/typings/types.d.ts +138 -30
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const Helper = require('@codeceptjs/helper');
7
7
  const Locator = require('../locator');
8
8
  const recorder = require('../recorder');
9
+ const store = require('../store');
9
10
  const stringIncludes = require('../assert/include').includes;
10
11
  const { urlEquals } = require('../assert/equal');
11
12
  const { equals } = require('../assert/equal');
@@ -33,6 +34,7 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
33
34
  const Popup = require('./extras/Popup');
34
35
  const Console = require('./extras/Console');
35
36
  const findReact = require('./extras/React');
37
+ const { highlightElement } = require('./scripts/highlightElement');
36
38
 
37
39
  let puppeteer;
38
40
  let perfTiming;
@@ -65,6 +67,7 @@ const consoleLogStore = new Console();
65
67
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
66
68
  * @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
67
69
  * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
70
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
68
71
  */
69
72
  const config = {};
70
73
 
@@ -1643,6 +1646,9 @@ class Puppeteer extends Helper {
1643
1646
  *
1644
1647
  * // passing in an array
1645
1648
  * I.type(['T', 'E', 'X', 'T']);
1649
+ *
1650
+ * // passing a secret
1651
+ * I.type(secret('123456'));
1646
1652
  * ```
1647
1653
  *
1648
1654
  * @param {string|string[]} key or array of keys to type.
@@ -1652,6 +1658,7 @@ class Puppeteer extends Helper {
1652
1658
  */
1653
1659
  async type(keys, delay = null) {
1654
1660
  if (!Array.isArray(keys)) {
1661
+ keys = keys.toString();
1655
1662
  keys = keys.split('');
1656
1663
  }
1657
1664
 
@@ -1692,7 +1699,10 @@ class Puppeteer extends Helper {
1692
1699
  } else if (editable) {
1693
1700
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1694
1701
  }
1702
+
1703
+ highlightActiveElement.call(this, el, this.page);
1695
1704
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1705
+
1696
1706
  return this._waitForAction();
1697
1707
  }
1698
1708
 
@@ -1718,6 +1728,8 @@ class Puppeteer extends Helper {
1718
1728
  *
1719
1729
  * ```js
1720
1730
  * I.appendField('#myTextField', 'appended');
1731
+ * // typing secret
1732
+ * I.appendField('password', secret('123456'));
1721
1733
  * ```
1722
1734
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
1723
1735
  * @param {string} value text value to append.
@@ -1729,8 +1741,9 @@ class Puppeteer extends Helper {
1729
1741
  async appendField(field, value) {
1730
1742
  const els = await findVisibleFields.call(this, field);
1731
1743
  assertElementExists(els, field, 'Field');
1744
+ highlightActiveElement.call(this, els[0], this.page);
1732
1745
  await els[0].press('End');
1733
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1746
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1734
1747
  return this._waitForAction();
1735
1748
  }
1736
1749
 
@@ -1831,6 +1844,7 @@ class Puppeteer extends Helper {
1831
1844
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1832
1845
  throw new Error('Element is not <select>');
1833
1846
  }
1847
+ highlightActiveElement.call(this, els[0], this.page);
1834
1848
  if (!Array.isArray(option)) option = [option];
1835
1849
 
1836
1850
  for (const key in option) {
@@ -2680,23 +2694,32 @@ class Puppeteer extends Helper {
2680
2694
  */
2681
2695
  async saveScreenshot(fileName, fullPage) {
2682
2696
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
2683
- const outputFile = screenshotOutputFolder(fileName);
2697
+ let outputFile = screenshotOutputFolder(fileName);
2684
2698
 
2685
2699
  this.debug(`Screenshot is saving to ${outputFile}`);
2686
2700
 
2701
+ await this.page.screenshot({
2702
+ path: outputFile,
2703
+ fullPage: fullPageOption,
2704
+ type: 'png',
2705
+ });
2706
+
2687
2707
  if (this.activeSessionName) {
2688
- const activeSessionPage = this.sessionPages[this.activeSessionName];
2689
-
2690
- if (activeSessionPage) {
2691
- return activeSessionPage.screenshot({
2692
- path: outputFile,
2693
- fullPage: fullPageOption,
2694
- type: 'png',
2695
- });
2708
+ for (const sessionName in this.sessionPages) {
2709
+ const activeSessionPage = this.sessionPages[sessionName];
2710
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2711
+
2712
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2713
+
2714
+ if (activeSessionPage) {
2715
+ await activeSessionPage.screenshot({
2716
+ path: outputFile,
2717
+ fullPage: fullPageOption,
2718
+ type: 'png',
2719
+ });
2720
+ }
2696
2721
  }
2697
2722
  }
2698
-
2699
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2700
2723
  }
2701
2724
 
2702
2725
  async _failed() {
@@ -3358,12 +3381,16 @@ async function proceedClick(locator, context = null, options = {}) {
3358
3381
  } else {
3359
3382
  assertElementExists(els, locator, 'Clickable element');
3360
3383
  }
3384
+
3385
+ highlightActiveElement.call(this, els[0], this.page);
3386
+
3361
3387
  await els[0].click(options);
3362
3388
  const promises = [];
3363
3389
  if (options.waitForNavigation) {
3364
3390
  promises.push(this.waitForNavigation());
3365
3391
  }
3366
3392
  promises.push(this._waitForAction());
3393
+
3367
3394
  return Promise.all(promises);
3368
3395
  }
3369
3396
 
@@ -3708,3 +3735,9 @@ function getNormalizedKey(key) {
3708
3735
  }
3709
3736
  return normalizedKey;
3710
3737
  }
3738
+
3739
+ function highlightActiveElement(element, context) {
3740
+ if (!this.options.enableHighlight && !store.debugMode) return;
3741
+
3742
+ highlightElement(element, context);
3743
+ }
@@ -34,7 +34,8 @@ const config = {};
34
34
  * endpoint: 'http://site.com/api',
35
35
  * prettyPrintJson: true,
36
36
  * onRequest: (request) => {
37
- * request.headers.auth = '123';
37
+ * request.headers.auth = '123';
38
+ * }
38
39
  * }
39
40
  * }
40
41
  *}
@@ -136,6 +137,15 @@ class REST extends Helper {
136
137
  request.auth = this.headers.auth;
137
138
  }
138
139
 
140
+ if (typeof request.data === 'object') {
141
+ const returnedValue = {};
142
+ for (const [key, value] of Object.entries(request.data)) {
143
+ returnedValue[key] = value;
144
+ if (value instanceof Secret) returnedValue[key] = value.getMasked();
145
+ }
146
+ _debugRequest.data = returnedValue;
147
+ }
148
+
139
149
  if (request.data instanceof Secret) {
140
150
  _debugRequest.data = '*****';
141
151
  request.data = (typeof request.data === 'object' && !(request.data instanceof Secret)) ? { ...request.data.toString() } : request.data.toString();
@@ -198,7 +208,7 @@ class REST extends Helper {
198
208
  * ```
199
209
  *
200
210
  * @param {*} url
201
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
211
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
202
212
  *
203
213
  * @returns {Promise<*>} response
204
214
  */
@@ -222,8 +232,8 @@ class REST extends Helper {
222
232
  * ```
223
233
  *
224
234
  * @param {*} url
225
- * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
226
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
235
+ * @param {*} [payload={}] - the payload to be sent. By default, it is sent as an empty object
236
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
227
237
  *
228
238
  * @returns {Promise<*>} response
229
239
  */
@@ -317,7 +327,7 @@ class REST extends Helper {
317
327
  * ```
318
328
  *
319
329
  * @param {*} url
320
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
330
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
321
331
  *
322
332
  * @returns {Promise<*>} response
323
333
  */
@@ -488,6 +488,8 @@ class TestCafe extends Helper {
488
488
  *
489
489
  * ```js
490
490
  * I.appendField('#myTextField', 'appended');
491
+ * // typing secret
492
+ * I.appendField('password', secret('123456'));
491
493
  * ```
492
494
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
493
495
  * @param {string} value text value to append.
@@ -501,7 +503,7 @@ class TestCafe extends Helper {
501
503
  const el = await els.nth(0);
502
504
 
503
505
  return this.t
504
- .typeText(el, value, { replace: false })
506
+ .typeText(el, value.toString(), { replace: false })
505
507
  .catch(mapError);
506
508
  }
507
509
 
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
 
7
7
  const Helper = require('@codeceptjs/helper');
8
+ const crypto = require('crypto');
8
9
  const stringIncludes = require('../assert/include').includes;
9
10
  const { urlEquals, equals } = require('../assert/equal');
10
11
  const { debug } = require('../output');
@@ -27,6 +28,8 @@ const {
27
28
  const ElementNotFound = require('./errors/ElementNotFound');
28
29
  const ConnectionRefused = require('./errors/ConnectionRefused');
29
30
  const Locator = require('../locator');
31
+ const { highlightElement } = require('./scripts/highlightElement');
32
+ const store = require('../store');
30
33
 
31
34
  const SHADOW = 'shadow';
32
35
  const webRoot = 'body';
@@ -39,7 +42,7 @@ const webRoot = 'body';
39
42
  * @typedef WebDriverConfig
40
43
  * @type {object}
41
44
  * @prop {string} url - base url of website to be tested.
42
- * @prop {string} browser browser in which to perform testing.
45
+ * @prop {string} browser - Browser in which to perform testing.
43
46
  * @prop {string} [basicAuth] - (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
44
47
  * @prop {string} [host=localhost] - WebDriver host to connect.
45
48
  * @prop {number} [port=4444] - WebDriver port to connect.
@@ -57,6 +60,7 @@ const webRoot = 'body';
57
60
  * @prop {object} [desiredCapabilities] Selenium's [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities).
58
61
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
59
62
  * @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
63
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
60
64
  */
61
65
  const config = {};
62
66
 
@@ -822,7 +826,7 @@ class WebDriver extends Helper {
822
826
  }
823
827
 
824
828
  /**
825
- * Find a checkbox by providing human readable text:
829
+ * Find a checkbox by providing human-readable text:
826
830
  *
827
831
  * ```js
828
832
  * this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ...
@@ -835,7 +839,7 @@ class WebDriver extends Helper {
835
839
  }
836
840
 
837
841
  /**
838
- * Find a clickable element by providing human readable text:
842
+ * Find a clickable element by providing human-readable text:
839
843
  *
840
844
  * ```js
841
845
  * const els = await this.helpers.WebDriver._locateClickable('Next page');
@@ -850,7 +854,7 @@ class WebDriver extends Helper {
850
854
  }
851
855
 
852
856
  /**
853
- * Find field elements by providing human readable text:
857
+ * Find field elements by providing human-readable text:
854
858
  *
855
859
  * ```js
856
860
  * this.helpers['WebDriver']._locateFields('Your email').then // ...
@@ -949,6 +953,7 @@ class WebDriver extends Helper {
949
953
  assertElementExists(res, locator, 'Clickable element');
950
954
  }
951
955
  const elem = usingFirstElement(res);
956
+ highlightActiveElement.call(this, elem);
952
957
  return this.browser[clickMethod](getElementId(elem));
953
958
  }
954
959
 
@@ -995,6 +1000,7 @@ class WebDriver extends Helper {
995
1000
  assertElementExists(res, locator, 'Clickable element');
996
1001
  }
997
1002
  const elem = usingFirstElement(res);
1003
+ highlightActiveElement.call(this, elem);
998
1004
 
999
1005
  return this.executeScript((el) => {
1000
1006
  if (document.activeElement instanceof HTMLElement) {
@@ -1035,6 +1041,7 @@ class WebDriver extends Helper {
1035
1041
  }
1036
1042
 
1037
1043
  const elem = usingFirstElement(res);
1044
+ highlightActiveElement.call(this, elem);
1038
1045
  return elem.doubleClick();
1039
1046
  }
1040
1047
 
@@ -1148,6 +1155,7 @@ class WebDriver extends Helper {
1148
1155
  const res = await findFields.call(this, field);
1149
1156
  assertElementExists(res, field, 'Field');
1150
1157
  const elem = usingFirstElement(res);
1158
+ highlightActiveElement.call(this, elem);
1151
1159
  return elem.setValue(value.toString());
1152
1160
  }
1153
1161
 
@@ -1157,6 +1165,8 @@ class WebDriver extends Helper {
1157
1165
  *
1158
1166
  * ```js
1159
1167
  * I.appendField('#myTextField', 'appended');
1168
+ * // typing secret
1169
+ * I.appendField('password', secret('123456'));
1160
1170
  * ```
1161
1171
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
1162
1172
  * @param {string} value text value to append.
@@ -1168,7 +1178,8 @@ class WebDriver extends Helper {
1168
1178
  const res = await findFields.call(this, field);
1169
1179
  assertElementExists(res, field, 'Field');
1170
1180
  const elem = usingFirstElement(res);
1171
- return elem.addValue(value);
1181
+ highlightActiveElement.call(this, elem);
1182
+ return elem.addValue(value.toString());
1172
1183
  }
1173
1184
 
1174
1185
  /**
@@ -1188,6 +1199,7 @@ class WebDriver extends Helper {
1188
1199
  const res = await findFields.call(this, field);
1189
1200
  assertElementExists(res, field, 'Field');
1190
1201
  const elem = usingFirstElement(res);
1202
+ highlightActiveElement.call(this, elem);
1191
1203
  return elem.clearValue(getElementId(elem));
1192
1204
  }
1193
1205
 
@@ -1219,6 +1231,7 @@ class WebDriver extends Helper {
1219
1231
  const res = await findFields.call(this, select);
1220
1232
  assertElementExists(res, select, 'Selectable field');
1221
1233
  const elem = usingFirstElement(res);
1234
+ highlightActiveElement.call(this, elem);
1222
1235
 
1223
1236
  if (!Array.isArray(option)) {
1224
1237
  option = [option];
@@ -1310,6 +1323,7 @@ class WebDriver extends Helper {
1310
1323
  assertElementExists(res, field, 'Checkable');
1311
1324
  const elem = usingFirstElement(res);
1312
1325
  const elementId = getElementId(elem);
1326
+ highlightActiveElement.call(this, elem);
1313
1327
 
1314
1328
  const isSelected = await this.browser.isElementSelected(elementId);
1315
1329
  if (isSelected) return Promise.resolve(true);
@@ -1342,6 +1356,7 @@ class WebDriver extends Helper {
1342
1356
  assertElementExists(res, field, 'Checkable');
1343
1357
  const elem = usingFirstElement(res);
1344
1358
  const elementId = getElementId(elem);
1359
+ highlightActiveElement.call(this, elem);
1345
1360
 
1346
1361
  const isSelected = await this.browser.isElementSelected(elementId);
1347
1362
  if (!isSelected) return Promise.resolve(true);
@@ -2689,6 +2704,9 @@ class WebDriver extends Helper {
2689
2704
  *
2690
2705
  * // passing in an array
2691
2706
  * I.type(['T', 'E', 'X', 'T']);
2707
+ *
2708
+ * // passing a secret
2709
+ * I.type(secret('123456'));
2692
2710
  * ```
2693
2711
  *
2694
2712
  * @param {string|string[]} key or array of keys to type.
@@ -2698,6 +2716,7 @@ class WebDriver extends Helper {
2698
2716
  */
2699
2717
  async type(keys, delay = null) {
2700
2718
  if (!Array.isArray(keys)) {
2719
+ keys = keys.toString();
2701
2720
  keys = keys.split('');
2702
2721
  }
2703
2722
  if (delay) {
@@ -4000,6 +4019,12 @@ function isModifierKey(key) {
4000
4019
  return unicodeModifierKeys.includes(key);
4001
4020
  }
4002
4021
 
4022
+ function highlightActiveElement(element) {
4023
+ if (!this.options.enableHighlight && !store.debugMode) return;
4024
+
4025
+ highlightElement(element, this.browser);
4026
+ }
4027
+
4003
4028
  function prepareLocateFn(context) {
4004
4029
  if (!context) return this._locate.bind(this);
4005
4030
  return (l) => {
package/docs/changelog.md CHANGED
@@ -7,6 +7,71 @@ layout: Section
7
7
 
8
8
  # Releases
9
9
 
10
+ ## 3.5.0
11
+
12
+ đŸ›Šī¸ Features
13
+
14
+ - **đŸĒ„ [AI Powered Test Automation](/ai)** - use OpenAI as a copilot for test automation. [#3713](https://github.com/codeceptjs/CodeceptJS/issues/3713) By **[davertmik](https://github.com/davertmik)**
15
+ ![](https://user-images.githubusercontent.com/220264/250418764-c382709a-3ccb-4eb5-b6bc-538f3b3b3d35.png)
16
+ * [AI guide](/ai) added
17
+ * added support for OpenAI in `pause()`
18
+ * added [`heal` plugin](/plugins#heal) for self-healing tests
19
+ * added [`OpenAI`](/helpers/openai) helper
20
+
21
+
22
+ - [Playwright][Puppeteer][WebDriver] Highlight the interacting elements in debug mode or with `highlightElement` option set ([#3672](https://github.com/codeceptjs/CodeceptJS/issues/3672)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
23
+
24
+ ![](https://user-images.githubusercontent.com/220264/250415226-a7620418-56a4-4837-b790-b15e91e5d1f0.png)
25
+
26
+ - **[Playwright]** Support for APIs in Playwright ([#3665](https://github.com/codeceptjs/CodeceptJS/issues/3665)) - by Egor Bodnar
27
+ * `clearField` replaced to use new Playwright API
28
+ * `blur` added
29
+ * `focus` added
30
+
31
+ - **[Added support for multiple browsers](/parallel#Parallel-Execution-by-Workers-on-Multiple-Browsers)** in `run-workers` ([#3606](https://github.com/codeceptjs/CodeceptJS/issues/3606)) by **[karanshah-browserstack](https://github.com/karanshah-browserstack)** :
32
+
33
+ Multiple browsers configured as profiles:
34
+
35
+ ```js
36
+ exports.config = {
37
+ helpers: {
38
+ WebDriver: {
39
+ url: 'http://localhost:3000',
40
+ }
41
+ },
42
+ multiple: {
43
+ profile1: {
44
+ browsers: [
45
+ {
46
+ browser: "firefox",
47
+ },
48
+ {
49
+ browser: "chrome",
50
+ }
51
+ ]
52
+ },
53
+ ```
54
+ And executed via `run-workers` with `all` argument
55
+
56
+ ```
57
+ npx codeceptjs run-workers 2 all
58
+ ```
59
+
60
+ - **[Appium]** Add Appium v2 support ([#3622](https://github.com/codeceptjs/CodeceptJS/issues/3622)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
61
+ - Improve `gpo` command to create page objects as modules or as classes ([#3625](https://github.com/codeceptjs/CodeceptJS/issues/3625)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
62
+ - Added `emptyOutputFolder` config to clean up output before running tests ([#3604](https://github.com/codeceptjs/CodeceptJS/issues/3604)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
63
+ - Add `secret()` function support to `append()` and `type()` ([#3615](https://github.com/codeceptjs/CodeceptJS/issues/3615)) - by **[anils92](https://github.com/anils92)**
64
+ - **[Playwright]** Add `bypassCSP` option to helper's config ([#3641](https://github.com/codeceptjs/CodeceptJS/issues/3641)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
65
+ - Print number of tests for each suite in dryRun ([#3620](https://github.com/codeceptjs/CodeceptJS/issues/3620)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
66
+
67
+ 🐛 Bug Fixes
68
+
69
+ - Support `--grep` in dry-run command ([#3673](https://github.com/codeceptjs/CodeceptJS/issues/3673)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
70
+ - Fix typings improvements in playwright ([#3650](https://github.com/codeceptjs/CodeceptJS/issues/3650)) - by **[KobeNguyenT](https://github.com/KobeNguyenT)**
71
+ - Fixed global retry [#3667](https://github.com/codeceptjs/CodeceptJS/issues/3667) by **[KobeNguyenT](https://github.com/KobeNguyenT)**
72
+ - Fixed creating JavaScript test using "codeceptjs gt" ([#3611](https://github.com/codeceptjs/CodeceptJS/issues/3611)) - by Jaromir Obr
73
+
74
+
10
75
  ## 3.4.1
11
76
 
12
77
  * Updated mocha to v 10.2. Fixes [#3591](https://github.com/codeceptjs/CodeceptJS/issues/3591)