Repository: codeceptjs/CodeceptJS (js)

Issue:
Passwords stored with secret() are written to output when using data-driven tests
#### What are you trying to achieve?

Using Data Driven Tests with an array of test data which contains credentials (passwords, secrets). I used the CodeceptJS function `secret()` to store them and expected to not see the passwords in the output (instead, see it masked like when not using data-driven tests).

Expected output is:
```
The test feature --
  The scenario | {"username":"Username","password": ***}
  ✔ OK in 7ms

  The scenario | {"username":"theSecondUsername","password": ***}
  ✔ OK in 1ms
```
(or similar)

#### What do you get instead?

The passwords are printed to the output.

Current output is:
```
The test feature --
  The scenario | {"username":"Username","password":{"_secret":"theSecretPassword"}}
  ✔ OK in 7ms

  The scenario | {"username":"theSecondUsername","password":{"_secret":"AnotherSecretPassword"}}
  ✔ OK in 1ms
```

Test source code I used:

```js
const testAccounts = new DataTable(['username', 'password']);
testAccounts.add(['Username', secret('theSecretPassword')]);
testAccounts.add(['theSecondUsername', secret('AnotherSecretPassword')]);

Feature('The test feature');
Data(testAccounts).Scenario('The scenario', ({ current }) => {
  ...
});
```

### Details

* CodeceptJS version: 3.3.3
* NodeJS Version: v15.14.0
* Operating System: MacOS

I took a quick look, but I am not confident:

This may be related to the function `replaceTitle` in [lib](https://github.com/codeceptjs/CodeceptJS/tree/3.x/lib)/[data](https://github.com/codeceptjs/CodeceptJS/tree/3.x/lib/data)/context.js
https://github.com/codeceptjs/CodeceptJS/blob/0903d05cb331b11edaddb87eb2787666dc231061/lib/data/context.js#L66

Pull request:
fix(datatable): mask secret text
## Motivation/Description of the PR
- Resolves #3429 

## Type of change
- [x] :bug: Bug fix


## Checklist:
- [x] Tests have been added
- [x] Local tests are passed (Run `npm test`)


Patch:
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 0ae7ed8a..77f68b52 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -30,11 +30,11 @@ Type: [object][16]
 -   `browser` **[string][17]** browser in which to perform testing.
 -   `basicAuth` **[string][17]?** (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
 -   `host` **[string][17]?** WebDriver host to connect.
--   `port` **[string][17]?** WebDriver port to connect.
+-   `port` **[number][20]?** WebDriver port to connect.
 -   `protocol` **[string][17]?** protocol for WebDriver server.
 -   `path` **[string][17]?** path to WebDriver server,
 -   `restart` **[boolean][29]?** restart browser between tests.
--   `smartWait` **[boolean][29]?** **enables [SmartWait][33]**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
+-   `smartWait` **([boolean][29] | [number][20])?** **enables [SmartWait][33]**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
 -   `disableScreenshots` **[boolean][29]?** don't save screenshots on failure.
 -   `fullPageScreenshots` **[boolean][29]?** (optional - make full page screenshots on failure.
 -   `uniqueScreenshotNames` **[boolean][29]?** option to prevent screenshot override if you have scenarios with the same name in different suites.
diff --git a/lib/data/context.js b/lib/data/context.js
index f41d89cc..8b26b76f 100644
--- a/lib/data/context.js
+++ b/lib/data/context.js
@@ -1,6 +1,7 @@
 const { isGenerator } = require('../utils');
 const DataTable = require('./table');
 const DataScenarioConfig = require('./dataScenarioConfig');
+const Secret = require('../secret');
 
 module.exports = function (context) {
   context.Data = function (dataTable) {
@@ -70,6 +71,12 @@ function replaceTitle(title, dataRow) {
   // it should be printed
   if (Object.prototype.toString.call(dataRow.data) === (Object()).toString()
     && dataRow.data.toString() !== (Object()).toString()) {
+    Object.entries(dataRow.data).forEach(entry => {
+      const [key, value] = entry;
+      if (value instanceof Secret) {
+        dataRow.data[key] = value.getMasked();
+      }
+    });
     return `${title} | ${dataRow.data}`;
   }
 
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 4c9e4bc7..828c55da 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -48,7 +48,7 @@ let version;
  * @prop {string} [protocol=http] - protocol for WebDriver server.
  * @prop {string} [path=/wd/hub] - path to WebDriver server,
  * @prop {boolean} [restart=true] - restart browser between tests.
- * @prop {boolean} [smartWait=false] - **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
+ * @prop {boolean|number} [smartWait=false] - **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
  * @prop {boolean} [disableScreenshots=false] - don't save screenshots on failure.
  * @prop {boolean} [fullPageScreenshots=false] (optional - make full page screenshots on failure.
  * @prop {boolean} [uniqueScreenshotNames=false] - option to prevent screenshot override if you have scenarios with the same name in different suites.


Test patch:
diff --git a/test/unit/data/ui_test.js b/test/unit/data/ui_test.js
index 9e8052c2..a95d4960 100644
--- a/test/unit/data/ui_test.js
+++ b/test/unit/data/ui_test.js
@@ -5,6 +5,7 @@ const Suite = require('mocha/lib/suite');
 const makeUI = require('../../../lib/ui');
 const addData = require('../../../lib/data/context');
 const DataTable = require('../../../lib/data/table');
+const Secret = require('../../../lib/secret');
 
 describe('ui', () => {
   let suite;
@@ -13,20 +14,23 @@ describe('ui', () => {
 
   beforeEach(() => {
     context = {};
-    suite = new Suite('empty');
+    suite = new Suite('empty', null);
     makeUI(suite);
     suite.emit('pre-require', context, {}, new Mocha());
     addData(context);
 
-    dataTable = new DataTable(['login', 'password']);
+    dataTable = new DataTable(['username', 'password']);
     dataTable.add(['jon', 'snow']);
     dataTable.xadd(['tyrion', 'lannister']);
     dataTable.add(['jaime', 'lannister']);
+    dataTable.add(['Username', new Secret('theSecretPassword')]);
   });
 
   describe('Data', () => {
     it('can add a tag to all scenarios', () => {
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable)
+        .Scenario('scenario', () => {
+        });
 
       dataScenarioConfig.tag('@user');
 
@@ -36,7 +40,7 @@ describe('ui', () => {
     });
 
     it('can add a timout to all scenarios', () => {
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
 
       dataScenarioConfig.timeout(3);
 
@@ -44,7 +48,7 @@ describe('ui', () => {
     });
 
     it('can add retries to all scenarios', () => {
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
 
       dataScenarioConfig.retry(3);
 
@@ -52,7 +56,7 @@ describe('ui', () => {
     });
 
     it('can expect failure for all scenarios', () => {
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
 
       dataScenarioConfig.fails();
 
@@ -62,7 +66,7 @@ describe('ui', () => {
     it('can expect a specific error for all scenarios', () => {
       const err = new Error();
 
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
 
       dataScenarioConfig.throws(err);
 
@@ -73,27 +77,28 @@ describe('ui', () => {
       const helperName = 'myHelper';
       const helper = {};
 
-      dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
 
       dataScenarioConfig.config(helperName, helper);
 
       dataScenarioConfig.scenarios.forEach(scenario => expect(helper).to.equal(scenario.test.config[helperName]));
     });
 
-    it("should shows object's toString() method in each scenario's name if the toString() method is overrided", () => {
-      const data = [
-        {
-          toString: () => 'test case title',
-        },
-      ];
-      const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {});
+    it("should shows object's toString() method in each scenario's name if the toString() method is overridden", () => {
+      const data = [{ toString: () => 'test case title' }];
+      const dataScenarioConfig = context.Data(data).Scenario('scenario', () => { });
       expect('scenario | test case title').to.equal(dataScenarioConfig.scenarios[0].test.title);
     });
 
-    it("should shows JSON.stringify() in each scenario's name if the toString() method isn't overrided", () => {
+    it("should shows JSON.stringify() in each scenario's name if the toString() method isn't overridden", () => {
       const data = [{ name: 'John Do' }];
       const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {});
       expect(`scenario | ${JSON.stringify(data[0])}`).to.equal(dataScenarioConfig.scenarios[0].test.title);
     });
+
+    it('should shows secret value as *****', () => {
+      const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {});
+      expect('scenario | {"username":"Username","password":"*****"}').to.equal(dataScenarioConfig.scenarios[2].test.title);
+    });
   });
 });