Repository: Codeception/CodeceptJS
Pull request #663 (2017-08-11 11:58:18)

Hooks generators
* https://github.com/Codeception/CodeceptJS/issues/206 Add generators support in scenario hooks BeforeSuite/Before/AfterSuite/After;
* Add Scenario Steps output for hooks, when they fails

DavertMik: Can we just not call them hooks? The term is [already in use](codecept.io/hooks/)

APshenkin: @DavertMik This term is from Mocha. Yep, we can change it, lets try to figure out suitable term
DavertMik: Ok, I couldn't think anything better :(
Let's have them as hook. If PR is ready we can merge it.
APshenkin: @DavertMik Yes, It's ready

Patch:
diff --git a/lib/event.js b/lib/event.js
index b67f3ac8..0e0ea8d7 100644
--- a/lib/event.js
+++ b/lib/event.js
@@ -15,6 +15,10 @@ module.exports = {
     before: 'suite.before',
     after: 'suite.after'
   },
+  hook: {
+    started: 'hook.start',
+    passed: 'hook.passed'
+  },
   step: {
     before: 'step.before', // async
     after: 'step.after',  // async
diff --git a/lib/listener/steps.js b/lib/listener/steps.js
index a9a46d87..aa0d35e5 100644
--- a/lib/listener/steps.js
+++ b/lib/listener/steps.js
@@ -3,7 +3,7 @@ const event = require('../event');
 const recorder = require('../recorder');
 const output = require('../output');
 
-let currentTest;
+let currentTest, currentHook;
 let steps;
 
 /**
@@ -22,16 +22,33 @@ module.exports = function () {
     currentTest = null;
   });
 
+  event.dispatcher.on(event.hook.passed, function () {
+    currentHook = null;
+  });
+
+  event.dispatcher.on(event.hook.started, function (suite) {
+    currentHook = suite.ctx.test;
+    currentHook.steps = [];
+  });
+
   event.dispatcher.on(event.test.failed, function (test) {
+    let cutSteps = function (current) {
+      let failureIndex = current.steps.findIndex((el) => {
+        return el.status === 'failed';
+      });
+      //To be sure that failed test will be failed in report
+      current.state = 'failed';
+      current.steps.length = failureIndex + 1;
+      return current;
+    };
+    if (currentHook && Array.isArray(currentHook.steps) && currentHook.steps.length) {
+      currentHook = cutSteps(currentHook);
+      return currentHook = null;
+    }
     if (!currentTest) return;
     // last step is failing step
     if (!currentTest.steps.length) return;
-    let failureIndex = currentTest.steps.findIndex((el) => {
-      return el.status === 'failed';
-    });
-    //To be sure that failed test will be failed in report
-    currentTest.state = 'failed';
-    currentTest.steps.length = failureIndex + 1;
+    return currentTest = cutSteps(currentTest);
   });
 
   event.dispatcher.on(event.test.passed, function (test) {
@@ -41,6 +58,9 @@ module.exports = function () {
   });
 
   event.dispatcher.on(event.step.before, function (step) {
+    if (currentHook && Array.isArray(currentHook.steps)) {
+      return currentHook.steps.push(step);
+    }
     if (!currentTest || !currentTest.steps) return;
     currentTest.steps.push(step);
   });
diff --git a/lib/listener/trace.js b/lib/listener/trace.js
index 0f32cee3..85951e4c 100644
--- a/lib/listener/trace.js
+++ b/lib/listener/trace.js
@@ -15,7 +15,7 @@ module.exports = function () {
     if (err instanceof AssertionFailedError) {
       msg = err.message = err.inspect();
     }
-    let steps = test.steps;
+    let steps = test.steps || test.ctx.test.steps;
     if (steps && steps.length) {
       let scenarioTrace = "";
       steps.reverse().forEach((step, i) => {
diff --git a/lib/scenario.js b/lib/scenario.js
index ba37842b..bf658138 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -102,14 +102,32 @@ module.exports.injected = function (fn, suite, hookName) {
       recorder.add(() => done(err));
     });
     try {
+      event.emit(event.hook.started, suite);
       recorder.startUnlessRunning();
       this.test.body = fn.toString();
-      fn.apply(this, getInjectedArguments(fn));
+      let res = fn.apply(this, getInjectedArguments(fn));
+      if (isGenerator(fn)) {
+        try {
+          res.next(); // running test
+        } catch (err) {
+          event.emit(event.test.failed, test, err);
+          done(err);
+        }
+        recorder.catch(); // catching possible errors in promises
+        resumeTest(res, () => {
+          recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
+          recorder.add(`finish ${hookName} hook`, () => done());
+        });
+      }
     } catch (err) {
       recorder.throw(err);
+    } finally {
+      if (!isGenerator(fn)) {
+        recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
+        recorder.add(`finish ${hookName} hook`, () => done());
+        recorder.catch();
+      }
     }
-    recorder.add(`finish ${hookName} hook`, () => done());
-    recorder.catch();
   };
 };
 


Test patch:
diff --git a/test/data/helper.js b/test/data/helper.js
index 9309630b..7173abfe 100644
--- a/test/data/helper.js
+++ b/test/data/helper.js
@@ -15,6 +15,10 @@ class MyHelper extends Helper {
     return 'hello dark side';
   }
 
+  stringWithHook(hookName) {
+    return `I'm generator ${hookName} hook`;
+  }
+
 }
 
 module.exports = MyHelper;
diff --git a/test/data/sandbox/codecept.testhooks.json b/test/data/sandbox/codecept.testhooks.json
new file mode 100644
index 00000000..d2aff000
--- /dev/null
+++ b/test/data/sandbox/codecept.testhooks.json
@@ -0,0 +1,14 @@
+{
+  "tests": "./*_test.testhooks.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "FakeDriver": {
+      "require": "../helper"
+    }
+  },
+  "include": {},
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
diff --git a/test/data/sandbox/testhooks_test.testhooks.js b/test/data/sandbox/testhooks_test.testhooks.js
new file mode 100644
index 00000000..97dadfca
--- /dev/null
+++ b/test/data/sandbox/testhooks_test.testhooks.js
@@ -0,0 +1,41 @@
+Feature('Test hooks');
+
+BeforeSuite(() => {
+  console.log(`I'm simple BeforeSuite hook`);
+});
+
+Before(() => {
+  console.log(`I'm simple Before hook`);
+});
+
+After(() => {
+  console.log(`I'm simple After hook`);
+});
+
+AfterSuite(() => {
+  console.log(`I'm simple AfterSuite hook`);
+});
+
+BeforeSuite(function* (I) {
+  let text = yield I.stringWithHook('BeforeSuite');
+  console.log(text);
+});
+
+Before(function* (I) {
+  let text = yield I.stringWithHook('Before');
+  console.log(text);
+});
+
+After(function* (I) {
+  let text = yield I.stringWithHook('After');
+  console.log(text);
+});
+
+AfterSuite(function* (I) {
+  let text = yield I.stringWithHook('AfterSuite');
+  console.log(text);
+});
+
+Scenario('Simple test 1', () => {
+  console.log(`It's first test`);
+});
diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js
index b8eb398b..0f950a36 100644
--- a/test/runner/codecept_test.js
+++ b/test/runner/codecept_test.js
@@ -1,5 +1,6 @@
 'use strict';
 let should = require('chai').should();
+let expect = require('chai').expect;
 let assert = require('assert');
 let path = require('path');
 const exec = require('child_process').exec;
@@ -85,6 +86,25 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
+  it('should run hooks from suites', (done) => {
+    exec(codecept_run_config('codecept.testhooks.json'), (err, stdout, stderr) => {
+      let lines = stdout.match(/\S.+/g);
+      expect(lines).to.include.members([
+        `I'm simple BeforeSuite hook`,
+        `I'm generator BeforeSuite hook`,
+        `I'm simple Before hook`,
+        `I'm generator Before hook`,
+        `I'm generator After hook`,
+        `I'm simple After hook`,
+        `I'm generator AfterSuite hook`,
+        `I'm simple AfterSuite hook`,
+      ]);
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
+
   it('should run bootstrap/teardown as object', (done) => {
     exec(codecept_run_config('codecept.bootstrap.obj.json'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
@@ -121,4 +141,4 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
-});
\ No newline at end of file
+});