Repository: Codeception/CodeceptJS
Pull request #1094 (2018-06-01 20:16:48)

Nested steps implementation
This enhancement allows displaying steps from page objects as well as their corresponding callers.

![selection_299](https://user-images.githubusercontent.com/220264/40861687-a3518a7a-65f2-11e8-9357-55be1ce40002.png)


codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- lib/step.js  7
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1726415&bid=7535169)
        
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- lib/step.js  7
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1726415&bid=7535169)
        
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- lib/step.js  7
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1726415&bid=7535169)
        

Patch:
diff --git a/lib/config.js b/lib/config.js
index 2f0c3002..46a09a0b 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -70,7 +70,10 @@ class Config {
   /**
    * Get current config.
    */
-  static get() {
+  static get(key, val) {
+    if (key) {
+      return config[key] || val;
+    }
     return config;
   }
 
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index 78f089e2..0574997c 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -573,12 +573,12 @@ class Nightmare extends Helper {
   async grabNumberOfVisibleElements(locator) {
     locator = new Locator(locator, 'css');
 
-    const els = await this.executeScript((by, locator) => {
+    const num = await this.browser.evaluate((by, locator) => {
       return window.codeceptjs.findElements(by, locator)
-        .filter(el => el.offsetWidth > 0 && el.offsetHeight > 0);
+        .filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
     }, locator.type, locator.value);
 
-    return els.length;
+    return num;
   }
 
   /**
diff --git a/lib/output.js b/lib/output.js
index d0896a70..80a73434 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -73,12 +73,14 @@ module.exports = {
   step(step) {
     if (outputLevel === 0) return;
     if (!step) return;
-    const sym = process.platform === 'win32' ? '*' : '•';
+    let stepLine = step.toString();
+    if (step.metaStep) stepLine = ' '.repeat(2) + colors.green(stepLine);
+    const sym = ' ';
     if (outputLevel === 2) {
       newline = false;
-      return process.stdout.write(`${' '.repeat(this.stepShift)} ${sym} ${step.toString()}`);
+      return process.stdout.write(`${' '.repeat(this.stepShift)} ${sym} ${stepLine}`);
     }
-    print(' '.repeat(this.stepShift), `${sym} ${step.toString()}`);
+    print(' '.repeat(this.stepShift), `${sym} ${stepLine}`);
   },
 
   /**
diff --git a/lib/reporter/cli.js b/lib/reporter/cli.js
index 9ac5e445..c7bb4c75 100644
--- a/lib/reporter/cli.js
+++ b/lib/reporter/cli.js
@@ -5,6 +5,7 @@ const AssertionFailedError = require('../assert/error');
 const output = require('../output');
 
 const cursor = Base.cursor;
+let currentMetaStep = null;
 
 class Cli extends Base {
   constructor(runner, opts) {
@@ -57,12 +58,23 @@ class Cli extends Base {
 
     if (showSteps) {
       runner.on('test', (test) => {
+        currentMetaStep = '';
         if (test.steps) {
           output.test.started(test);
         }
       });
 
-      event.dispatcher.on(event.step.started, step => output.step(step));
+      event.dispatcher.on(event.step.started, (step) => {
+        if (step.metaStep) {
+          if (currentMetaStep !== step.metaStep.toString()) {
+            output.step(step.metaStep);
+            currentMetaStep = step.metaStep.toString();
+          } else {
+            currentMetaStep = '';
+          }
+        }
+        output.step(step);
+      });
       event.dispatcher.on(event.step.passed, step => output.stepExecutionTime(step));
       event.dispatcher.on(event.step.failed, step => output.stepExecutionTime(step));
     }
diff --git a/lib/step.js b/lib/step.js
index a169b763..0558a469 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -1,3 +1,16 @@
+const Config = require('./config');
+const path = require('path');
+
+const support = Config.get('include', {});
+if (support) {
+  for (const name in support) {
+    const file = support[name];
+    support[name] = path.join(global.codecept_dir, file);
+  }
+}
+
+const STACK_LINE = 3;
+
 /**
  * Each command in test executed through `I.` object is wrapped in Step.
  * Step allows logging executed commands and triggers hook before and after step execution.
@@ -12,7 +25,12 @@ class Step {
     this.prefix = this.suffix = '';
     this.args = [];
     this.stack = '';
+    this.setTrace();
+  }
+
+  setTrace() {
     Error.captureStackTrace(this);
+    this.metaStep = detectMetaStep(this.stack.split('\n'));
   }
 
   setArguments(args) {
@@ -69,7 +87,7 @@ class Step {
   line() {
     const lines = this.stack.split('\n');
     // 3rd line is line where step has been called in test
-    if (lines[3]) return lines[3].trim();
+    if (lines[STACK_LINE]) return lines[STACK_LINE].trim();
     return '';
   }
 
@@ -83,3 +101,44 @@ class Step {
 }
 
 module.exports = Step;
+
+class MetaStep extends Step {
+  constructor(obj, method) {
+    super(null, method);
+    this.actor = obj;
+  }
+
+  setTrace() {
+  }
+
+  run() {
+  }
+}
+
+
+function detectMetaStep(stack) {
+  for (let i = STACK_LINE; i < stack.length; i++) {
+    const line = stack[i].trim();
+    if (!isTest(line)) continue;
+    if (i === STACK_LINE) return;
+    for (const name in support) {
+      const file = support[name];
+      const caller = stack[i - 1].trim();
+      // const caller = stack[i-1].trim().match(/^at (\w+\.\w+)\s\(/);
+      if (caller.indexOf(file) > -1) {
+        // console.log('YEAAAH!!!!', name);
+        let fnName = caller.match(/^at \w+\.(.*?)\(/);
+        if (fnName[1]) {
+          fnName = fnName[1].trim();
+        }
+        // console.log(name, fnName);
+        return new MetaStep(name, fnName);
+      }
+    }
+    return;
+  }
+}
+
+function isTest(line) {
+  return line.trim().match(/^at Test\.Scenario/);
+}
diff --git a/lib/within.js b/lib/within.js
index dfa9be47..eabb9947 100644
--- a/lib/within.js
+++ b/lib/within.js
@@ -8,14 +8,10 @@ const resumeTest = require('./scenario').resumeTest;
 function within(context, fn) {
   const helpers = container.helpers();
   const locator = typeof context === 'object' ? JSON.stringify(context) : context;
-  const addContextToStep = function (step) {
-    step.prefix = `Within ${locator}: `;
-  };
 
   return recorder.add('register within wrapper', () => {
     recorder.session.start('within');
 
-    event.dispatcher.on(event.step.after, addContextToStep);
     output.stepShift = 2;
     if (output.level() > 0) output.print(`   Within ${output.colors.yellow.bold(locator)}:`);
 
@@ -26,7 +22,6 @@ function within(context, fn) {
     const finalize = () => {
       recorder.add('Finalize session within session', () => {
         output.stepShift = 1;
-        event.dispatcher.removeListener(event.step.after, addContextToStep);
         recorder.session.restore('within');
       });
     };
@@ -58,14 +53,12 @@ function within(context, fn) {
         }
         recorder.catch((err) => {
           output.stepShift = 1;
-          event.dispatcher.removeListener(event.step.after, addContextToStep);
           throw err;
         }); // catching possible errors in promises
         resumeTest(res, () => {
           finishHelpers();
         }, (err) => {
           output.stepShift = 1;
-          event.dispatcher.removeListener(event.step.after, addContextToStep);
           throw err;
         }, true);
       }
@@ -76,7 +69,6 @@ function within(context, fn) {
         finishHelpers();
         recorder.catch((err) => {
           output.stepShift = 1;
-          event.dispatcher.removeListener(event.step.after, addContextToStep);
           throw err;
         });
       }


Test patch:
diff --git a/test/data/sandbox/codecept.po.json b/test/data/sandbox/codecept.po.json
new file mode 100644
index 00000000..a0641a40
--- /dev/null
+++ b/test/data/sandbox/codecept.po.json
@@ -0,0 +1,15 @@
+{
+  "tests": "./*_test.po.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "FileSystem": {}
+  },
+  "include": {
+    "I": "./support/custom_steps.js",
+    "MyPage": "./support/my_page.js"
+  },
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
\ No newline at end of file
diff --git a/test/data/sandbox/fs_test.po.js b/test/data/sandbox/fs_test.po.js
new file mode 100644
index 00000000..5dcc894a
--- /dev/null
+++ b/test/data/sandbox/fs_test.po.js
@@ -0,0 +1,8 @@
+Feature('Filesystem');
+
+Scenario('check current dir', (I, MyPage) => {
+  I.openDir('aaa');
+  I.seeFile('codecept.json');
+  MyPage.hasFile('uu');
+  I.seeFile('codecept.po.json');
+});
diff --git a/test/data/sandbox/support/custom_steps.js b/test/data/sandbox/support/custom_steps.js
new file mode 100644
index 00000000..b5a509cd
--- /dev/null
+++ b/test/data/sandbox/support/custom_steps.js
@@ -0,0 +1,8 @@
+module.exports = () => {
+  return actor({
+    openDir() {
+      this.amInPath('.');
+    },
+  });
+};
+
diff --git a/test/data/sandbox/support/my_page.js b/test/data/sandbox/support/my_page.js
new file mode 100644
index 00000000..dfb00c59
--- /dev/null
+++ b/test/data/sandbox/support/my_page.js
@@ -0,0 +1,13 @@
+let I;
+
+module.exports = {
+
+  _init() {
+    I = actor();
+  },
+
+  hasFile() {
+    I.seeFile('codecept.json');
+    I.seeFile('codecept.po.json');
+  },
+};
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index ba0f34d2..0abb8171 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -143,6 +143,12 @@ module.exports.tests = function () {
         xpath: '//div[@id = "grab-multiple"]//a',
       }))
       .then(num => assert.equal(num, 3)));
+    it('should grab number of visible elements for given css locator', () => I.amOnPage('/info')
+      .then(() => I.grabNumberOfVisibleElements('[id=grab-multiple] a'))
+      .then(num => assert.equal(num, 3)));
+    it('should return 0 for non-existing elements', () => I.amOnPage('/info')
+      .then(() => I.grabNumberOfVisibleElements('button[type=submit]'))
+      .then(num => assert.equal(num, 0)));
   });
 
   describe('#seeInSource, #dontSeeInSource', () => {
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 43719172..d4c8b695 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -80,7 +80,7 @@ describe('CodeceptJS Interface', () => {
         'Emitted | step.before (I am in path ".")',
         'Emitted | step.after (I am in path ".")',
         'Emitted | step.start (I am in path ".")',
-        '• I am in path "."',
+        'I am in path "."',
       ];
 
       lines.filter(l => beforeStep.indexOf(l) > -1)
@@ -88,9 +88,9 @@ describe('CodeceptJS Interface', () => {
 
       // steps order
       const step = [
-        '• I am in path "."',
+        'I am in path "."',
         'hello world',
-        '• I see file "codecept.json"',
+        'I see file "codecept.json"',
       ];
 
       lines.filter(l => step.indexOf(l) > -1)
@@ -100,4 +100,24 @@ describe('CodeceptJS Interface', () => {
       done();
     });
   });
+
+
+  it('should display meta steps and substeps', (done) => {
+    exec(`${config_run_config('codecept.po.json')} --steps`, (err, stdout) => {
+      const lines = stdout.split('\n');
+      lines.should.include.members([
+        ' check current dir',
+        '   I open dir ',
+        '     I am in path "."',
+        '   I see file "codecept.json"',
+        '   MyPage has file ',
+        '     I see file "codecept.json"',
+        '     I see file "codecept.po.json"',
+        '   I see file "codecept.po.json"',
+      ]);
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
 });
diff --git a/test/runner/run_multiple_test.js b/test/runner/run_multiple_test.js
index afcfe738..164e8f1e 100644
--- a/test/runner/run_multiple_test.js
+++ b/test/runner/run_multiple_test.js
@@ -80,10 +80,10 @@ describe('CodeceptJS Multiple Runner', function () {
   it('should print steps', (done) => {
     exec(`${codecept_run}default --steps`, (err, stdout, stderr) => {
       stdout.should.include('CodeceptJS'); // feature
-      stdout.should.include('[2.default:firefox] print browser ');
-      stdout.should.include('[2.default:firefox]  • I print browser');
-      stdout.should.include('[1.default:chrome] print browser ');
-      stdout.should.include('[1.default:chrome]  • I print browser');
+      stdout.should.include('[2.default:firefox]  print browser ');
+      stdout.should.include('[2.default:firefox]    I print browser');
+      stdout.should.include('[1.default:chrome]  print browser ');
+      stdout.should.include('[1.default:chrome]    I print browser');
       assert(!err);
       done();
     });
diff --git a/test/runner/session_test.js b/test/runner/session_test.js
index b5481aca..0f98e31e 100644
--- a/test/runner/session_test.js
+++ b/test/runner/session_test.js
@@ -22,15 +22,15 @@ describe('CodeceptJS session', function () {
       testStatus = list.pop();
       testStatus.should.include('OK');
       list.should.eql([
-        '• I do "writing"',
-        '• davert: I do "reading"',
-        '• I do "playing"',
-        '• john: I do "crying"',
-        '• davert: I do "smiling"',
-        '• I do "laughing"',
-        '• mike: I do "spying"',
-        '• john: I do "lying"',
-        '• I do "waving"',
+        'I do "writing"',
+        'davert: I do "reading"',
+        'I do "playing"',
+        'john: I do "crying"',
+        'davert: I do "smiling"',
+        'I do "laughing"',
+        'mike: I do "spying"',
+        'john: I do "lying"',
+        'I do "waving"',
       ], 'check steps execution order');
       done();
     });
@@ -46,13 +46,13 @@ describe('CodeceptJS session', function () {
       testStatus.should.include('OK');
 
       list.should.eql([
-        '• I do "writing"',
-        '• I do "playing"',
-        '• john: I do "crying"',
-        '• davert: I do "smiling"',
-        '• I do "laughing"',
-        '• davert: I do "singing"',
-        '• I do "waving"',
+        'I do "writing"',
+        'I do "playing"',
+        'john: I do "crying"',
+        'davert: I do "smiling"',
+        'I do "laughing"',
+        'davert: I do "singing"',
+        'I do "waving"',
       ], 'check steps execution order');
       done();
     });
diff --git a/test/runner/within_test.js b/test/runner/within_test.js
index 977b9c72..51a25e2b 100644
--- a/test/runner/within_test.js
+++ b/test/runner/within_test.js
@@ -23,13 +23,13 @@ describe('CodeceptJS within', function () {
       testStatus = withoutGeneratorList.pop();
       testStatus.should.include('OK');
       withoutGeneratorList.should.eql([
-        '• I small promise ',
-        '• small Promise was finished',
+        'I small promise ',
+        'small Promise was finished',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• oh! I am within end(',
+        'Hey! I am within Begin. I get blabla',
+        'I small promise ',
+        'small Promise was finished',
+        'oh! I am within end(',
       ], 'check steps execution order');
       done();
     });
@@ -43,17 +43,17 @@ describe('CodeceptJS within', function () {
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
-        '• I small promise ',
-        '• small Promise was finished',
-        '• I small yield ',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small yield ',
+        'Hey! I am within Begin. I get blabla',
+        'I small yield ',
         'I am small yield string',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• oh! I am within end(',
+        'I small promise ',
+        'small Promise was finished',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();
@@ -68,17 +68,17 @@ describe('CodeceptJS within', function () {
       testStatus = withGeneratorListOtherOrder.pop();
       testStatus.should.include('OK');
       withGeneratorListOtherOrder.should.eql([
-        '• I small promise ',
-        '• small Promise was finished',
-        '• I small yield ',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• Within blabla: I small yield ',
+        'Hey! I am within Begin. I get blabla',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string',
-        '• oh! I am within end(',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();
@@ -93,17 +93,17 @@ describe('CodeceptJS within', function () {
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
-        '• I small promise ',
-        '• small Promise was finished',
-        '• I small yield ',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string await',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small yield ',
+        'Hey! I am within Begin. I get blabla',
+        'I small yield ',
         'I am small yield string await',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• oh! I am within end(',
+        'I small promise ',
+        'small Promise was finished',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();
@@ -118,17 +118,17 @@ describe('CodeceptJS within', function () {
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
-        '• I small promise ',
-        '• small Promise was finished',
-        '• I small yield ',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string await',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• Within blabla: I small yield ',
+        'Hey! I am within Begin. I get blabla',
+        'I small promise ',
+        'small Promise was finished',
+        'I small yield ',
         'I am small yield string await',
-        '• oh! I am within end(',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();
@@ -144,17 +144,17 @@ describe('CodeceptJS within', function () {
       testStatus = withGeneratorListWithContinued.pop();
       testStatus.should.include('OK');
       withGeneratorListWithContinued.should.eql([
-        '• I small yield ',
+        'I small yield ',
         'I am small yield string',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I small yield ',
+        'Hey! I am within Begin. I get blabla',
+        'I small yield ',
         'I am small yield string',
-        '• Within blabla: I small promise ',
-        '• small Promise was finished',
-        '• oh! I am within end(',
-        '• I small promise ',
-        '• small Promise was finished',
+        'I small promise ',
+        'small Promise was finished',
+        'oh! I am within end(',
+        'I small promise ',
+        'small Promise was finished',
       ], 'check steps execution order');
       done();
     });
@@ -168,12 +168,12 @@ describe('CodeceptJS within', function () {
       testStatus = errorInWithinList.pop();
       testStatus.should.include('FAILED');
       errorInWithinList.should.eql([
-        '• I small yield ',
+        'I small yield ',
         'I am small yield string',
         'Within blabla:',
-        '• Hey! I am within Begin. I get blabla',
-        '• Within blabla: I error step ',
-        '• oh! I am within end(',
+        'Hey! I am within Begin. I get blabla',
+        'I error step ',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();
@@ -188,8 +188,8 @@ describe('CodeceptJS within', function () {
       testStatus = errorInTestList.pop();
       testStatus.should.include('FAILED');
       errorInTestList.should.eql([
-        '• I error step ',
-        '• oh! I am within end(',
+        'I error step ',
+        'oh! I am within end(',
       ], 'check steps execution order');
 
       done();