codeceptjs 3.4.1 → 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.
Files changed (75) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +11 -9
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +248 -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 +234 -54
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +101 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +61 -2
  14. package/docs/build/WebDriver.js +85 -5
  15. package/docs/changelog.md +85 -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 +228 -151
  21. package/docs/helpers/Puppeteer.md +153 -101
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +97 -49
  24. package/docs/helpers/WebDriver.md +159 -107
  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/blur.mustache +17 -0
  32. package/docs/webapi/focus.mustache +12 -0
  33. package/docs/webapi/type.mustache +3 -0
  34. package/lib/ai.js +171 -0
  35. package/lib/cli.js +10 -2
  36. package/lib/codecept.js +4 -0
  37. package/lib/command/dryRun.js +9 -1
  38. package/lib/command/generate.js +46 -3
  39. package/lib/command/init.js +23 -1
  40. package/lib/command/interactive.js +15 -1
  41. package/lib/command/run-workers.js +2 -1
  42. package/lib/container.js +13 -3
  43. package/lib/event.js +2 -0
  44. package/lib/helper/Appium.js +45 -7
  45. package/lib/helper/JSONResponse.js +4 -4
  46. package/lib/helper/Nightmare.js +1 -1
  47. package/lib/helper/OpenAI.js +122 -0
  48. package/lib/helper/Playwright.js +200 -45
  49. package/lib/helper/Protractor.js +1 -1
  50. package/lib/helper/Puppeteer.js +67 -12
  51. package/lib/helper/REST.js +15 -5
  52. package/lib/helper/TestCafe.js +30 -2
  53. package/lib/helper/WebDriver.js +51 -5
  54. package/lib/helper/scripts/blurElement.js +17 -0
  55. package/lib/helper/scripts/focusElement.js +17 -0
  56. package/lib/helper/scripts/highlightElement.js +20 -0
  57. package/lib/html.js +258 -0
  58. package/lib/interfaces/gherkin.js +8 -0
  59. package/lib/listener/retry.js +2 -1
  60. package/lib/pause.js +73 -17
  61. package/lib/plugin/debugErrors.js +67 -0
  62. package/lib/plugin/fakerTransform.js +4 -6
  63. package/lib/plugin/heal.js +177 -0
  64. package/lib/plugin/screenshotOnFail.js +11 -2
  65. package/lib/recorder.js +11 -8
  66. package/lib/secret.js +5 -4
  67. package/lib/step.js +6 -1
  68. package/lib/ui.js +4 -3
  69. package/lib/utils.js +17 -0
  70. package/lib/workers.js +57 -9
  71. package/package.json +25 -16
  72. package/translations/ja-JP.js +9 -9
  73. package/typings/index.d.ts +43 -9
  74. package/typings/promiseBasedTypes.d.ts +242 -25
  75. package/typings/types.d.ts +260 -35
package/lib/pause.js CHANGED
@@ -1,9 +1,12 @@
1
1
  const colors = require('chalk');
2
2
  const readline = require('readline');
3
+ const ora = require('ora-classic');
4
+ const debug = require('debug')('codeceptjs:pause');
3
5
 
4
6
  const container = require('./container');
5
7
  const history = require('./history');
6
8
  const store = require('./store');
9
+ const AiAssistant = require('./ai');
7
10
  const recorder = require('./recorder');
8
11
  const event = require('./event');
9
12
  const output = require('./output');
@@ -15,6 +18,8 @@ let nextStep;
15
18
  let finish;
16
19
  let next;
17
20
  let registeredVariables = {};
21
+ const aiAssistant = new AiAssistant();
22
+
18
23
  /**
19
24
  * Pauses test execution and starts interactive shell
20
25
  */
@@ -45,6 +50,14 @@ function pauseSession(passedObject = {}) {
45
50
  output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
46
51
  output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`));
47
52
  output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`));
53
+
54
+ if (aiAssistant.isEnabled) {
55
+ output.print(colors.blue(` ${colors.bold('OpenAI is enabled! (experimental)')} Write what you want and make OpenAI run it`));
56
+ output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to OpenAI'));
57
+ output.print(colors.blue(' Ideas: ask it to fill forms for you or to click'));
58
+ } else {
59
+ output.print(colors.blue(` Enable OpenAI assistant by setting ${colors.bold('OPENAI_API_KEY')} env variable`));
60
+ }
48
61
  }
49
62
  rl = readline.createInterface(process.stdin, process.stdout, completer);
50
63
 
@@ -59,9 +72,10 @@ function pauseSession(passedObject = {}) {
59
72
  }
60
73
 
61
74
  /* eslint-disable */
62
- function parseInput(cmd) {
75
+ async function parseInput(cmd) {
63
76
  rl.pause();
64
77
  next = false;
78
+ recorder.session.start('pause');
65
79
  store.debugMode = false;
66
80
  if (cmd === '') next = true;
67
81
  if (!cmd || cmd === 'resume' || cmd === 'exit') {
@@ -74,37 +88,78 @@ function parseInput(cmd) {
74
88
  for (const k of Object.keys(registeredVariables)) {
75
89
  eval(`var ${k} = registeredVariables['${k}'];`); // eslint-disable-line no-eval
76
90
  }
91
+
92
+ let executeCommand = Promise.resolve();
93
+
94
+ const getCmd = () => {
95
+ debug('Command:', cmd)
96
+ return cmd;
97
+ };
98
+
77
99
  store.debugMode = true;
78
100
  let isCustomCommand = false;
79
101
  let lastError = null;
102
+ let isAiCommand = false;
103
+ let $res;
80
104
  try {
81
105
  const locate = global.locate; // enable locate in this context
82
106
  const I = container.support('I');
83
107
  if (cmd.trim().startsWith('=>')) {
84
108
  isCustomCommand = true;
85
109
  cmd = cmd.trim().substring(2, cmd.length);
110
+ } else if (aiAssistant.isEnabled && !cmd.match(/^\w+\(/) && cmd.includes(' ')) {
111
+ const currentOutputLevel = output.level();
112
+ output.level(0);
113
+ const res = I.grabSource();
114
+ isAiCommand = true;
115
+ executeCommand = executeCommand.then(async () => {
116
+ try {
117
+ const html = await res;
118
+ aiAssistant.setHtmlContext(html);
119
+ } catch (err) {
120
+ output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
121
+ return;
122
+ } finally {
123
+ output.level(currentOutputLevel);
124
+ }
125
+ // aiAssistant.mockResponse("```js\nI.click('Sign in');\n```");
126
+ const spinner = ora("Processing OpenAI request...").start();
127
+ cmd = await aiAssistant.writeSteps(cmd);
128
+ spinner.stop();
129
+ output.print('');
130
+ output.print(colors.blue(aiAssistant.getResponse()));
131
+ output.print('');
132
+ return cmd;
133
+ })
86
134
  } else {
87
135
  cmd = `I.${cmd}`;
88
136
  }
89
- const executeCommand = eval(cmd); // eslint-disable-line no-eval
90
-
91
- const result = executeCommand instanceof Promise ? executeCommand : Promise.resolve(executeCommand);
92
- result.then((val) => {
93
- if (isCustomCommand) {
94
- console.log(val);
95
- return;
96
- }
97
- if (cmd.startsWith('I.see') || cmd.startsWith('I.dontSee')) {
98
- output.print(output.styles.success(' OK '), cmd);
99
- return;
100
- }
101
- if (cmd.startsWith('I.grab')) {
102
- output.print(output.styles.debug(val));
103
- }
137
+ executeCommand = executeCommand.then(async () => {
138
+ const cmd = getCmd();
139
+ if (!cmd) return;
140
+ return eval(cmd); // eslint-disable-line no-eval
104
141
  }).catch((err) => {
142
+ debug(err);
143
+ if (isAiCommand) return;
105
144
  if (!lastError) output.print(output.styles.error(' ERROR '), err.message);
145
+ debug(err.stack)
146
+
106
147
  lastError = err.message;
107
- });
148
+ })
149
+
150
+ const val = await executeCommand;
151
+
152
+ if (isCustomCommand) {
153
+ if (val !== undefined) console.log('Result', '$res=', val); // eslint-disable-line
154
+ $res = val;
155
+ }
156
+
157
+ if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
158
+ output.print(output.styles.success(' OK '), cmd);
159
+ }
160
+ if (cmd?.startsWith('I.grab')) {
161
+ output.print(output.styles.debug(val));
162
+ }
108
163
 
109
164
  history.push(cmd); // add command to history when successful
110
165
  } catch (err) {
@@ -117,6 +172,7 @@ function parseInput(cmd) {
117
172
  // pop latest command from history because it failed
118
173
  history.pop();
119
174
 
175
+ if (isAiCommand) return;
120
176
  if (!lastError) output.print(output.styles.error(' FAIL '), msg);
121
177
  lastError = err.message;
122
178
  });
@@ -0,0 +1,67 @@
1
+ const Container = require('../container');
2
+ const recorder = require('../recorder');
3
+ const event = require('../event');
4
+ const supportedHelpers = require('./standardActingHelpers');
5
+ const { scanForErrorMessages } = require('../html');
6
+ const { output } = require('..');
7
+
8
+ const defaultConfig = {
9
+ errorClasses: ['error', 'warning', 'alert', 'danger'],
10
+ };
11
+
12
+ /**
13
+ * Prints errors found in HTML code after each failed test.
14
+ *
15
+ * It scans HTML and searches for elements with error classes.
16
+ * If an element found prints a text from it to console and adds as artifact to the test.
17
+ *
18
+ * Enable this plugin in config:
19
+ *
20
+ * ```js
21
+ * plugins: {
22
+ * debugErrors: {
23
+ * enabled: true,
24
+ * }
25
+ * ```
26
+ *
27
+ * Additional config options:
28
+ *
29
+ * * `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`)
30
+ *
31
+ */
32
+ module.exports = function (config = {}) {
33
+ const helpers = Container.helpers();
34
+ let helper;
35
+
36
+ config = Object.assign(defaultConfig, config);
37
+
38
+ for (const helperName of supportedHelpers) {
39
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
40
+ helper = helpers[helperName];
41
+ }
42
+ }
43
+
44
+ if (!helper) return; // no helpers for screenshot
45
+
46
+ event.dispatcher.on(event.test.failed, (test) => {
47
+ recorder.add('HTML snapshot failed test', async () => {
48
+ try {
49
+ const currentOutputLevel = output.level();
50
+ output.level(0);
51
+ const html = await helper.grabHTMLFrom('body');
52
+ output.level(currentOutputLevel);
53
+
54
+ if (!html) return;
55
+
56
+ const errors = scanForErrorMessages(html, config.errorClasses);
57
+ if (errors.length) {
58
+ output.debug('Detected errors in HTML code');
59
+ errors.forEach((error) => output.debug(error));
60
+ test.artifacts.errors = errors;
61
+ }
62
+ } catch (err) {
63
+ // not really needed
64
+ }
65
+ });
66
+ });
67
+ };
@@ -2,20 +2,18 @@ const { faker } = require('@faker-js/faker');
2
2
  const transform = require('../transform');
3
3
 
4
4
  /**
5
- * Use the [faker.js](https://www.npmjs.com/package/faker) package to generate fake data inside examples on your gherkin tests
6
- *
7
- * ![Faker.js](https://raw.githubusercontent.com/Marak/faker.js/master/logo.png)
5
+ * Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests
8
6
  *
9
7
  * #### Usage
10
8
  *
11
- * To start please install `faker.js` package
9
+ * To start please install `@faker-js/faker` package
12
10
  *
13
11
  * ```
14
- * npm install -D faker
12
+ * npm install -D @faker-js/faker
15
13
  * ```
16
14
  *
17
15
  * ```
18
- * yarn add -D faker
16
+ * yarn add -D @faker-js/faker
19
17
  * ```
20
18
  *
21
19
  * Add this plugin to config file:
@@ -0,0 +1,177 @@
1
+ const debug = require('debug')('codeceptjs:heal');
2
+ const colors = require('chalk');
3
+ const Container = require('../container');
4
+ const AiAssistant = require('../ai');
5
+ const recorder = require('../recorder');
6
+ const event = require('../event');
7
+ const output = require('../output');
8
+ const supportedHelpers = require('./standardActingHelpers');
9
+
10
+ const defaultConfig = {
11
+ healLimit: 2,
12
+ healSteps: [
13
+ 'click',
14
+ 'fillField',
15
+ 'appendField',
16
+ 'selectOption',
17
+ 'attachFile',
18
+ 'checkOption',
19
+ 'uncheckOption',
20
+ 'doubleClick',
21
+ ],
22
+ };
23
+
24
+ /**
25
+ * Self-healing tests with OpenAI.
26
+ *
27
+ * This plugin is experimental and requires OpenAI API key.
28
+ *
29
+ * To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
30
+ *
31
+ * ```js
32
+ * plugins: {
33
+ * heal: {
34
+ * enabled: true,
35
+ * }
36
+ * }
37
+ * ```
38
+ *
39
+ * More config options are available:
40
+ *
41
+ * * `healLimit` - how many steps can be healed in a single test (default: 2)
42
+ * * `healSteps` - which steps can be healed (default: all steps that interact with UI, see list below)
43
+ *
44
+ * Steps to heal:
45
+ *
46
+ * * `click`
47
+ * * `fillField`
48
+ * * `appendField`
49
+ * * `selectOption`
50
+ * * `attachFile`
51
+ * * `checkOption`
52
+ * * `uncheckOption`
53
+ * * `doubleClick`
54
+ *
55
+ */
56
+ module.exports = function (config = {}) {
57
+ const aiAssistant = new AiAssistant();
58
+
59
+ let currentTest = null;
60
+ let currentStep = null;
61
+ let healedSteps = 0;
62
+
63
+ const healSuggestions = [];
64
+
65
+ config = Object.assign(defaultConfig, config);
66
+
67
+ event.dispatcher.on(event.test.before, (test) => {
68
+ currentTest = test;
69
+ healedSteps = 0;
70
+ });
71
+
72
+ event.dispatcher.on(event.step.started, step => currentStep = step);
73
+
74
+ event.dispatcher.on(event.step.before, () => {
75
+ const store = require('../store');
76
+ if (store.debugMode) return;
77
+
78
+ recorder.catchWithoutStop(async (err) => {
79
+ if (!aiAssistant.isEnabled) throw err;
80
+ if (!currentStep) throw err;
81
+ if (!config.healSteps.includes(currentStep.name)) throw err;
82
+ const test = currentTest;
83
+
84
+ if (healedSteps >= config.healLimit) {
85
+ output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
86
+ output.print('Entire flow can be broken, please check it manually');
87
+ output.print('or increase healing limit in heal plugin config');
88
+
89
+ throw err;
90
+ }
91
+
92
+ recorder.session.start('heal');
93
+ const helpers = Container.helpers();
94
+ let helper;
95
+
96
+ for (const helperName of supportedHelpers) {
97
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
98
+ helper = helpers[helperName];
99
+ }
100
+ }
101
+
102
+ if (!helper) throw err; // no helpers for html
103
+
104
+ const step = test.steps[test.steps.length - 1];
105
+ debug('Self-healing started', step.toCode());
106
+
107
+ const currentOutputLevel = output.level();
108
+ output.level(0);
109
+ const html = await helper.grabHTMLFrom('body');
110
+ output.level(currentOutputLevel);
111
+
112
+ if (!html) throw err;
113
+
114
+ aiAssistant.setHtmlContext(html);
115
+ await tryToHeal(step, err);
116
+ recorder.session.restore();
117
+ });
118
+ });
119
+
120
+ event.dispatcher.on(event.all.result, () => {
121
+ if (!healSuggestions.length) return;
122
+
123
+ const { print } = output;
124
+
125
+ print('');
126
+ print('===================');
127
+ print(colors.bold.green('Self-Healing Report:'));
128
+
129
+ print(`${colors.bold(healSuggestions.length)} step(s) were healed by AI`);
130
+
131
+ let i = 1;
132
+ print('');
133
+ print('Suggested changes:');
134
+ print('');
135
+
136
+ for (const suggestion of healSuggestions) {
137
+ print(`${i}. To fix ${colors.bold.blue(suggestion.test.title)}`);
138
+ print('Replace the failed code with:');
139
+ print(colors.red(`- ${suggestion.step.toCode()}`));
140
+ print(colors.green(`+ ${suggestion.snippet}`));
141
+ print(suggestion.step.line());
142
+ print('');
143
+ i++;
144
+ }
145
+ });
146
+
147
+ async function tryToHeal(failedStep, err) {
148
+ output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
149
+
150
+ const codeSnippets = await aiAssistant.healFailedStep(
151
+ failedStep, err, currentTest,
152
+ );
153
+
154
+ output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
155
+
156
+ for (const codeSnippet of codeSnippets) {
157
+ try {
158
+ debug('Executing', codeSnippet);
159
+ await eval(codeSnippet); // eslint-disable-line
160
+
161
+ healSuggestions.push({
162
+ test: currentTest,
163
+ step: failedStep,
164
+ snippet: codeSnippet,
165
+ });
166
+
167
+ output.print(colors.bold.green(' Code healed successfully'));
168
+ healedSteps++;
169
+ return;
170
+ } catch (err) {
171
+ debug('Failed to execute code', err);
172
+ }
173
+ }
174
+
175
+ output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
176
+ }
177
+ };
@@ -75,7 +75,8 @@ module.exports = function (config) {
75
75
  event.dispatcher.on(event.test.failed, (test) => {
76
76
  recorder.add('screenshot of failed test', async () => {
77
77
  let fileName = clearString(test.title);
78
- // This prevent data driven to be included in the failed screenshot file name
78
+ const dataType = 'image/png';
79
+ // This prevents data driven to be included in the failed screenshot file name
79
80
  if (fileName.indexOf('{') !== -1) {
80
81
  fileName = fileName.substr(0, (fileName.indexOf('{') - 3)).trim();
81
82
  }
@@ -106,7 +107,15 @@ module.exports = function (config) {
106
107
 
107
108
  const allureReporter = Container.plugins('allure');
108
109
  if (allureReporter) {
109
- allureReporter.addAttachment('Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), 'image/png');
110
+ allureReporter.addAttachment('Main session - Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), dataType);
111
+
112
+ if (helper.activeSessionName) {
113
+ for (const sessionName in helper.sessionPages) {
114
+ const screenshotFileName = `${sessionName}_${fileName}`;
115
+ test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName);
116
+ allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType);
117
+ }
118
+ }
110
119
  }
111
120
 
112
121
  const cucumberReporter = Container.plugins('cucumberJsonReporter');
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;
@@ -161,7 +162,7 @@ module.exports = {
161
162
  * true: it will retries if `retryOpts` set.
162
163
  * false: ignore `retryOpts` and won't retry.
163
164
  * @param {number} [timeout]
164
- * @return {Promise<*> | undefined}
165
+ * @return {Promise<*>}
165
166
  * @inner
166
167
  */
167
168
  add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
@@ -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);
@@ -250,9 +252,9 @@ module.exports = {
250
252
  err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
251
253
  }
252
254
  if (customErrFn) {
253
- customErrFn(err);
254
- } else if (errFn) {
255
- errFn(err);
255
+ return customErrFn(err);
256
+ } if (errFn) {
257
+ return errFn(err);
256
258
  }
257
259
  });
258
260
  },
@@ -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/secret.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable max-classes-per-file */
2
2
  const { deepClone } = require('./utils');
3
3
 
4
+ const maskedString = '*****';
5
+
4
6
  /** @param {string} secret */
5
7
  class Secret {
6
8
  constructor(secret) {
@@ -13,7 +15,7 @@ class Secret {
13
15
  }
14
16
 
15
17
  getMasked() {
16
- return '*****';
18
+ return maskedString;
17
19
  }
18
20
 
19
21
  /**
@@ -36,12 +38,11 @@ function secretObject(obj, fieldsToHide = []) {
36
38
  if (prop === 'toString') {
37
39
  return function () {
38
40
  const maskedObject = deepClone(obj);
39
- fieldsToHide.forEach(f => maskedObject[f] = '****');
41
+ fieldsToHide.forEach(f => maskedObject[f] = maskedString);
40
42
  return JSON.stringify(maskedObject);
41
43
  };
42
44
  }
43
-
44
- return obj[prop];
45
+ return fieldsToHide.includes(prop) ? new Secret(obj[prop]) : obj[prop];
45
46
  },
46
47
  };
47
48
 
package/lib/step.js CHANGED
@@ -172,7 +172,12 @@ class Step {
172
172
  } else if (arg.toString && arg.toString() !== '[object Object]') {
173
173
  return arg.toString();
174
174
  } else if (typeof arg === 'object') {
175
- return JSON.stringify(arg);
175
+ const returnedArg = {};
176
+ for (const [key, value] of Object.entries(arg)) {
177
+ returnedArg[key] = value;
178
+ if (value instanceof Secret) returnedArg[key] = value.getMasked();
179
+ }
180
+ return JSON.stringify(returnedArg);
176
181
  }
177
182
  return arg;
178
183
  }).join(', ');
package/lib/ui.js CHANGED
@@ -23,10 +23,11 @@ const setContextTranslation = (context) => {
23
23
  /**
24
24
  * Codecept-style interface:
25
25
  *
26
- * Feature('login')
27
- * Scenario('login as regular user', (I) {
26
+ * Feature('login');
27
+ *
28
+ * Scenario('login as regular user', ({I}) {
28
29
  * I.fillField();
29
- * I.click()
30
+ * I.click();
30
31
  * I.see('Hello, '+data.login);
31
32
  * });
32
33
  *
package/lib/utils.js CHANGED
@@ -455,3 +455,20 @@ module.exports.isNotSet = function (obj) {
455
455
  if (obj === undefined) return true;
456
456
  return false;
457
457
  };
458
+
459
+ module.exports.emptyFolder = async (directoryPath) => {
460
+ require('child_process').execSync(`rm -rf ${directoryPath}/*`);
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
+ };