Repository: Codeception/CodeceptJS
Pull request #463 (2017-04-08 20:37:32)

Implemented Plugins API for Codeceptjs
Plugins can be added to config using "hooks" config option.

Plugins can assign to event hooks, use container, recorder, etc.
Current listeners were refactored to use same API.

```js
let event = require('codeceptjs').event;
let output = require('codeceptjs').output;

module.exports = function() {

  event.dispatcher.on(event.test.before, function () {
     output.print(`hello, I'm before test`);
  });

}
```

This plugin can be added to config by pointing to a module:

```js
"hooks": [
   "./plugin.js"
]
```


Patch:
diff --git a/lib/codecept.js b/lib/codecept.js
index 45de10c9..b990c41b 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -11,6 +11,7 @@ let event = require('../lib/event');
 let glob = require('glob');
 let fileExists = require('./utils').fileExists;
 let runHook = require('./hooks');
+
 const scenarioUi = fsPath.join(__dirname, './interfaces/bdd.js');
 
 class Codecept {
@@ -42,22 +43,28 @@ class Codecept {
 
     // initializing listeners
     container.create(this.config);
-    require('./listener/steps');
-    require('./listener/helpers');
-    require('./listener/exit');
-    require('./listener/trace');
-
     this.bootstrap(callback);
   }
 
   // loading bootstrap
   bootstrap(done) {
-    runHook('bootstrap', this.config, done);
+
+    // default hooks
+    runHook(require('./listener/steps'));
+    runHook(require('./listener/helpers'));
+    runHook(require('./listener/exit'));
+    runHook(require('./listener/trace'));
+
+    // bootstrap
+    runHook(this.config.bootstrap, done, 'bootstrap');
+
+    // custom hooks
+    (this.config.hooks || []).forEach((hook) => runHook(hook, done, 'bootstrap'));
   }
 
   // loading teardown
   teardown(done) {
-    runHook('teardown', this.config, done);
+    runHook(this.config.teardown, done, 'teardown');
   }
 
   loadTests(pattern) {
@@ -103,4 +110,4 @@ class Codecept {
   }
 }
 
-module.exports = Codecept;
+module.exports = Codecept;
\ No newline at end of file
diff --git a/lib/container.js b/lib/container.js
index 64a89965..ca446796 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -3,6 +3,49 @@ let path = require('path');
 let fileExists = require('./utils').fileExists;
 let Translation = require('./translation');
 
+let container = {
+  helpers: {},
+  support: {},
+  translation: {}
+};
+
+module.exports = {
+
+  create: (config) => {
+    container.helpers = createHelpers(config.helpers || {});
+    container.translation = loadTranslation(config.translation || null);
+    container.support = createSupportObjects(config.include || {});
+  },
+
+  support: (name) => {
+    if (!name) {
+      return container.support;
+    }
+    return container.support[name];
+  },
+
+  helpers: (name) => {
+    if (!name) {
+      return container.helpers;
+    }
+    return container.helpers[name];
+  },
+
+  translation: () => {
+    return container.translation;
+  },
+
+  append: (newContainer) => {
+    this.container = Object.assign(this.container, newContainer);
+  },
+
+  clear: (newHelpers, newSupport) => {
+    container.helpers = newHelpers || {};
+    container.support = newSupport || {};
+    container.translation = loadTranslation();
+  }
+};
+
 function createHelpers(config) {
   let helpers = {};
   let helperModule;
@@ -61,8 +104,8 @@ function createSupportObjects(config) {
   if (!objects.I) {
     objects.I = require('./actor')();
 
-    if (translation.I != 'I') {
-      objects[translation.I] = objects.I;
+    if (container.translation.I != 'I') {
+      objects[container.translation.I] = objects.I;
     }
   }
 
@@ -91,41 +134,3 @@ function loadTranslation(translation) {
 
   return new Translation(vocabulary);
 }
-
-let helpers = {};
-let support = {};
-let translation = {};
-
-module.exports = {
-
-  create: (config) => {
-    helpers = createHelpers(config.helpers || {});
-    translation = loadTranslation(config.translation || null);
-    support = createSupportObjects(config.include || {});
-  },
-
-  support: (name) => {
-    if (!name) {
-      return support;
-    }
-    return support[name];
-  },
-
-  helpers: (name) => {
-    if (!name) {
-      return helpers;
-    }
-    return helpers[name];
-  },
-
-  translation: () => {
-    return translation;
-  },
-
-  clear: (newHelpers, newSupport) => {
-    helpers = newHelpers || {};
-    support = newSupport || {};
-    translation = loadTranslation();
-  }
-
-};
diff --git a/lib/hooks.js b/lib/hooks.js
index 029817dd..9df15372 100644
--- a/lib/hooks.js
+++ b/lib/hooks.js
@@ -4,19 +4,21 @@ let getParamNames = require('./utils').getParamNames;
 let fsPath = require('path');
 let fileExists = require('./utils').fileExists;
 
-module.exports = function (hook, config, done) {
-  if (typeof config[hook] === 'string' && fileExists(fsPath.join(codecept_dir, config[hook]))) {
-    var callable = require(fsPath.join(codecept_dir, config[hook]));
+module.exports = function (hook, done, stage) {
+  stage = stage || 'bootstrap';
+  if (typeof hook === 'string' && fileExists(fsPath.join(codecept_dir, hook))) {
+    var callable = require(fsPath.join(codecept_dir, hook));
     if (typeof callable === 'function') {
       callSync(callable, done);
       return;
     }
-    if (typeof callable === 'object' && callable[hook]) {
-      callSync(callable[hook], done);
+    if (typeof callable === 'object' && callable[stage]) {
+      callSync(callable[stage], done);
       return;
     }
-  } else if (typeof config[hook] === 'function') {
-    callSync(config[hook], done);
+    console.log(callable);
+  } else if (typeof hook === 'function') {
+    callSync(hook, done);
     return;
   }
   // if async - call done
@@ -28,7 +30,7 @@ function callSync(callable, done) {
     callable(done);
   } else {
     callable();
-    done();
+    if (done) done();
   }
 }
 
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 00000000..501e82cf
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,13 @@
+'use strict';
+/**
+ * Index file for loading CodeceptJS programmatically.
+ *
+ * Includes Public API objects
+ */
+module.exports = {
+  Codecept: require('./codecept'),
+  output: require('./output'),
+  container: require('./container'),
+  event: require('./event'),
+  recorder: require('./recorder')
+}
\ No newline at end of file
diff --git a/lib/listener/exit.js b/lib/listener/exit.js
index bb0f8fbc..062c417d 100644
--- a/lib/listener/exit.js
+++ b/lib/listener/exit.js
@@ -1,26 +1,29 @@
 'use strict';
 
-let event = require('../event');
+const event = require('../event');
 
-let failed = false;
+module.exports = function() {
 
-let failedTests = [];
+  let failed = false;
 
-event.dispatcher.on(event.test.failed, function (testOrSuite) {
-  // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
-  // is a suite and not a test
-  failedTests.push(testOrSuite.fullTitle());
-});
+  let failedTests = [];
 
-// if test was successful after retries
-event.dispatcher.on(event.test.passed, function (testOrSuite) {
-  // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
-  // is a suite and not a test
-  failedTests = failedTests.filter((failed) => testOrSuite.fullTitle() !== failed);
-});
+  event.dispatcher.on(event.test.failed, function (testOrSuite) {
+    // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
+    // is a suite and not a test
+    failedTests.push(testOrSuite.fullTitle());
+  });
 
-event.dispatcher.on(event.all.result, function () {
-  if (failedTests.length) {
-    process.exitCode = 1;
-  }
-});
+  // if test was successful after retries
+  event.dispatcher.on(event.test.passed, function (testOrSuite) {
+    // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
+    // is a suite and not a test
+    failedTests = failedTests.filter((failed) => testOrSuite.fullTitle() !== failed);
+  });
+
+  event.dispatcher.on(event.all.result, function () {
+    if (failedTests.length) {
+      process.exitCode = 1;
+    }
+  });
+}
diff --git a/lib/listener/helpers.js b/lib/listener/helpers.js
index 95bb5c26..11b24d71 100644
--- a/lib/listener/helpers.js
+++ b/lib/listener/helpers.js
@@ -1,60 +1,65 @@
 'use strict';
 
-let container = require('../container');
-let event = require('../event');
-let recorder = require('../recorder');
-let debug = require('../output').debug;
+const event = require('../event');
+const container = require('../container');
+const recorder = require('../recorder');
+const debug = require('../output').debug;
 
-let helpers = container.helpers();
+/**
+ * Enable Helpers to listen to test events
+ */
+module.exports = function() {
+  let helpers = container.helpers();
 
-let runHelpersHook = (hook, param) => {
-  Object.keys(helpers).forEach((key) => {
-    helpers[key][hook](param);
-  });
-};
+  const runHelpersHook = (hook, param) => {
+    Object.keys(helpers).forEach((key) => {
+      helpers[key][hook](param);
+    });
+  };
 
-let runAsyncHelpersHook = (hook, param, force) => {
-  Object.keys(helpers).forEach((key) => {
-    if (!helpers[key][hook]) return;
-    recorder.add(`hook ${key}.${hook}()`, () => helpers[key][hook](param), force);
-  });
-};
+  const runAsyncHelpersHook = (hook, param, force) => {
+    Object.keys(helpers).forEach((key) => {
+      if (!helpers[key][hook]) return;
+      recorder.add(`hook ${key}.${hook}()`, () => helpers[key][hook](param), force);
+    });
+  };
 
-event.dispatcher.on(event.all.before, function () {
-  runHelpersHook('_init');
-});
+  event.dispatcher.on(event.all.before, function () {
+    runHelpersHook('_init');
+  });
 
-event.dispatcher.on(event.suite.before, function (suite) {
-  runAsyncHelpersHook('_beforeSuite', null, true);
-});
+  event.dispatcher.on(event.suite.before, function (suite) {
+    runAsyncHelpersHook('_beforeSuite', null, true);
+  });
 
-event.dispatcher.on(event.suite.after, function (suite) {
-  runAsyncHelpersHook('_afterSuite', null, true);
-});
+  event.dispatcher.on(event.suite.after, function (suite) {
+    runAsyncHelpersHook('_afterSuite', null, true);
+  });
 
-event.dispatcher.on(event.test.started, function (test) {
-  runHelpersHook('_test', test);
-  recorder.catch((e) => debug(e));
-});
+  event.dispatcher.on(event.test.started, function (test) {
+    runHelpersHook('_test', test);
+    recorder.catch((e) => debug(e));
+  });
 
-event.dispatcher.on(event.test.before, function () {
-  runAsyncHelpersHook('_before');
-});
+  event.dispatcher.on(event.test.before, function () {
+    runAsyncHelpersHook('_before');
+  });
 
-event.dispatcher.on(event.test.failed, function (test) {
-  runAsyncHelpersHook('_failed', test, true);
-  // should not fail test execution, so errors should be catched
-  recorder.catch((e) => debug(e));
-});
+  event.dispatcher.on(event.test.failed, function (test) {
+    runAsyncHelpersHook('_failed', test, true);
+    // should not fail test execution, so errors should be catched
+    recorder.catch((e) => debug(e));
+  });
 
-event.dispatcher.on(event.test.after, function (test) {
-  runAsyncHelpersHook('_after', {}, true);
-});
+  event.dispatcher.on(event.test.after, function (test) {
+    runAsyncHelpersHook('_after', {}, true);
+  });
 
-event.dispatcher.on(event.step.before, function (step) {
-  runAsyncHelpersHook('_beforeStep', step);
-});
+  event.dispatcher.on(event.step.before, function (step) {
+    runAsyncHelpersHook('_beforeStep', step);
+  });
 
-event.dispatcher.on(event.step.after, function (step) {
-  runAsyncHelpersHook('_afterStep', step);
-});
+  event.dispatcher.on(event.step.after, function (step) {
+    runAsyncHelpersHook('_afterStep', step);
+  });
+}
diff --git a/lib/listener/steps.js b/lib/listener/steps.js
index c448be40..e9438e36 100644
--- a/lib/listener/steps.js
+++ b/lib/listener/steps.js
@@ -1,27 +1,33 @@
 'use strict';
-
-let event = require('../event');
+const event = require('../event');
 
 let currentTest;
 let steps;
 
-event.dispatcher.on(event.test.started, function (test) {
-  currentTest = test;
-  currentTest.steps = [];
-});
+/**
+ * Register steps inside tests
+ */
+module.exports = function() {
+
+  event.dispatcher.on(event.test.started, function (test) {
+    currentTest = test;
+    currentTest.steps = [];
+  });
+
+  event.dispatcher.on(event.test.after, function () {
+    currentTest = null;
+  });
 
-event.dispatcher.on(event.test.after, function () {
-  currentTest = null;
-});
+  event.dispatcher.on(event.test.failed, function (test) {
+    if (!currentTest) return;
+    // last step is failing step
+    if (!currentTest.steps.length) return;
+    currentTest.steps.slice(-1)[0].status = 'failed';
+  });
 
-event.dispatcher.on(event.test.failed, function (test) {
-  if (!currentTest) return;
-  // last step is failing step
-  if (!currentTest.steps.length) return;
-  currentTest.steps.slice(-1)[0].status = 'failed';
-});
+  event.dispatcher.on(event.step.before, function (step) {
+    if (!currentTest || !currentTest.steps) return;
+    currentTest.steps.push(step);
+  });
+}
 
-event.dispatcher.on(event.step.before, function (step) {
-  if (!currentTest || !currentTest.steps) return;
-  currentTest.steps.push(step);
-});
diff --git a/lib/listener/trace.js b/lib/listener/trace.js
index ae637bf4..87f6d27b 100644
--- a/lib/listener/trace.js
+++ b/lib/listener/trace.js
@@ -1,30 +1,38 @@
 'use strict';
 
-let event = require('../event');
-let AssertionFailedError = require('../assert/error');
-let output = require('../output');
-let ucfirst = require('../utils').ucfirst;
+const output = require('../output');
+const event = require('../event');
+const AssertionFailedError = require('../assert/error');
+const ucfirst = require('../utils').ucfirst;
 
-event.dispatcher.on(event.test.failed, function (test, err) {
-  let msg = err.message;
-  if (err instanceof AssertionFailedError) {
-    msg = err.message = err.inspect();
-  }
-  let steps = test.steps;
-  if (steps && steps.length) {
-    let scenarioTrace = "";
-    steps.reverse().forEach((step, i) => {
-      let line = `- ${step.toCode()} ${step.line()}`;
-      // if (step.status === 'failed') line = '' + line;
-      scenarioTrace += "\n" + line;
-    });
-    msg += `\n\nScenario Steps:\n${scenarioTrace}\n\n`;
-  }
+/**
+ * Register stack trace for scenarios
+ */
+module.exports = function() {
 
-  if (output.level() < 3) {
-    err.stack = ''; // hide internal error stack trace in non-verbose mode
-  }
+  event.dispatcher.on(event.test.failed, function (test, err) {
+    let msg = err.message;
+    if (err instanceof AssertionFailedError) {
+      msg = err.message = err.inspect();
+    }
+    let steps = test.steps;
+    if (steps && steps.length) {
+      let scenarioTrace = "";
+      steps.reverse().forEach((step, i) => {
+        let line = `- ${step.toCode()} ${step.line()}`;
+        // if (step.status === 'failed') line = '' + line;
+        scenarioTrace += "\n" + line;
+      });
+      msg += `\n\nScenario Steps:\n${scenarioTrace}\n\n`;
+    }
+
+    if (output.level() < 3) {
+      err.stack = ''; // hide internal error stack trace in non-verbose mode
+    }
+
+    err.stack = msg + err.stack;
+    test.err = err;
+  });
+
+}
 
-  err.stack = msg + err.stack;
-  test.err = err;
-});
diff --git a/package.json b/package.json
index 93d1366e..96ff7a64 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "lib",
     "translations"
   ],
-  "main": "lib/codecept.js",
+  "main": "lib/index.js",
   "keywords": [
     "tdd",
     "bdd",


Test patch:
diff --git a/test/data/sandbox/codecept.hooks.obj.json b/test/data/sandbox/codecept.bootstrap.obj.json
similarity index 100%
rename from test/data/sandbox/codecept.hooks.obj.json
rename to test/data/sandbox/codecept.bootstrap.obj.json
diff --git a/test/data/sandbox/codecept.hooks.js b/test/data/sandbox/codecept.hooks.js
new file mode 100644
index 00000000..0ff0980d
--- /dev/null
+++ b/test/data/sandbox/codecept.hooks.js
@@ -0,0 +1,17 @@
+module.exports.config = {
+  "tests": "./*_test.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "FileSystem": {}
+  },
+  "include": {},
+  "hooks": [
+    "bootstrap.sync.js",
+    function() {
+      console.log('I am function hook');
+    }
+  ],
+  "mocha": {},
+  "name": "sandbox"
+}
\ No newline at end of file
diff --git a/test/data/sandbox/steps.d.ts b/test/data/sandbox/steps.d.ts
new file mode 100644
index 00000000..3b729b37
--- /dev/null
+++ b/test/data/sandbox/steps.d.ts
@@ -0,0 +1,29 @@
+
+type ICodeceptCallback = (i: CodeceptJS.I) => void;
+
+declare const actor: () => CodeceptJS.I;
+declare const Feature: (string: string) => void;
+declare const Scenario: (string: string, callback: ICodeceptCallback) => void;
+declare const Before: (callback: ICodeceptCallback) => void;
+declare const After: (callback: ICodeceptCallback) => void;
+declare const within: (selector: string, callback: Function) => void;
+
+declare namespace CodeceptJS {
+  export interface I {
+    amInPath: (openPath) => any; 
+    writeToFile: (name, text) => any; 
+    seeFile: (name) => any; 
+    seeInThisFile: (text, encoding) => any; 
+    dontSeeInThisFile: (text, encoding) => any; 
+    seeFileContentsEqual: (text, encoding) => any; 
+    dontSeeFileContentsEqual: (text, encoding) => any; 
+    debug: (msg) => any; 
+    debugSection: (section, msg) => any; 
+    say: (msg) => any; 
+
+  }
+}
+
+declare module "codeceptjs" {
+    export = CodeceptJS;
+}
diff --git a/test/runner/config_test.js b/test/runner/codecept_test.js
similarity index 88%
rename from test/runner/config_test.js
rename to test/runner/codecept_test.js
index 6a8f17a0..dd64cfb1 100644
--- a/test/runner/config_test.js
+++ b/test/runner/codecept_test.js
@@ -63,8 +63,18 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
+  it('should run hooks', (done) => {
+    exec(config_run_config('codecept.hooks.js'), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('I am function hook');
+      assert(!err);
+      done();
+    });
+  });
+
   it('should run bootstrap/teardown as object', (done) => {
-    exec(config_run_config('codecept.hooks.obj.json'), (err, stdout, stderr) => {
+    exec(config_run_config('codecept.bootstrap.obj.json'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       stdout.should.include('I am bootstrap');
       stdout.should.include('I am teardown');
diff --git a/test/runner/list_test.js b/test/runner/list_test.js
index 3855fc86..52bb3e65 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -11,7 +11,6 @@ describe('list/def commands', () => {
 
   it('list should print actions', (done) => {
     exec(`${runner} list ${codecept_dir}`, (err, stdout, stderr) => {
-      console.log(stdout);
       stdout.should.include('FileSystem'); // helper name
       stdout.should.include('FileSystem I.amInPath(openPath)'); // action name
       stdout.should.include('FileSystem I.seeFile(name)');