Repository: Codeception/CodeceptJS
Pull request #1712 (2019-06-11 07:27:07)

German translation
I fixed the translation file to use underscore between words and all lowercase words.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64edd832..9915d6b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,7 @@
 * [Puppeteer] Fixed `UnhandledPromiseRejectionWarning: "Execution context was destroyed...` by @adrielcodeco
 * [WebDriver] Keep browser window dimensions when starting a new session by @spiroid
 * Replace Ghekrin plceholders with values in files that combine a scenerio outline and table by @medtoure18.
-* Added Documentation to [locate elements in React Native](https://codecept.io/react-native-locators) apps. By @DimGun.
+* Added Documentation to [locate elements in React Native](https://codecept.io/mobile-react-native-locators) apps. By @DimGun.
 * Adding optional `path` parameter to `bdd:snippets` command to append snippets to a specific file. By @cthorsen31.
 * Added optional `output` parameter to `def` command by @LukoyanovE.
 * [Puppeteer] Added `grabDataFromPerformanceTiming` by @PeterNgTr.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c54edcf9..0de634d6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,6 +18,14 @@ To run codeceptjs from this repo use:
 node bin/codecept.js
 ```
 
+To run examples:
+
+
+```
+node bin/codecept.js run -c examples
+```
+
+
 Depending on a type of a change you should do the following.
 
 ## Helpers
diff --git a/docs/acceptance.md b/docs/acceptance.md
index 632eb42c..704a1ce4 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -364,3 +364,46 @@ Function passed into session can use `I`, page objects, and any objects declared
 This function can also be declared as async (but doesn't work as generator).
 
 Also, you can use `within` inside a session but you can't call session from inside `within`.
+
+## Multiple Windows
+
+CodeceptJS allows to use several browser windows inside a test. Sometimes we are testing the functionality of websites that we cannot control, such as a closed-source managed package, and there are popups that either remain open for configuring data on the screen, or close as a result of clicking a window. We can use these functions in order to gain more control over which page is being tested with Codecept at any given time. For example:
+
+```js
+const assert = require('assert');
+
+Scenario('should open main page of configured site, open a popup, switch to main page, then switch to popup, close popup, and go back to main page', async (I) => {
+    I.amOnPage('/');
+    const handleBeforePopup = await I.grabCurrentWindowHandle();
+    const urlBeforePopup = await I.grabCurrentUrl();
+    const allHandlesBeforePopup = await I.grabAllWindowHandles();
+    assert.equal(allHandlesBeforePopup.length, 1, 'Single Window');
+
+    await I.executeScript(() => {
+        window.open('https://www.w3schools.com/', 'new window', 'toolbar=yes,scrollbars=yes,resizable=yes,width=400,height=400');
+    });
+
+    const allHandlesAfterPopup = await I.grabAllWindowHandles();    
+    assert.equal(allHandlesAfterPopup.length, 2, 'Two Windows');
+
+    await I.switchToWindow(allHandlesAfterPopup[1]);
+    const urlAfterPopup = await I.grabCurrentUrl();    
+    assert.equal(urlAfterPopup, 'https://www.w3schools.com/', 'Expected URL: Popup');
+    
+    assert.equal(handleBeforePopup, allHandlesAfterPopup[0], 'Expected Window: Main Window');
+    await I.switchToWindow(handleBeforePopup);
+    const currentURL = await I.grabCurrentUrl();
+    assert.equal(currentURL, urlBeforePopup, 'Expected URL: Main URL');    
+
+    await I.switchToWindow(allHandlesAfterPopup[1]);
+    const urlAfterSwitchBack = await I.grabCurrentUrl();
+    assert.equal(urlAfterSwitchBack, 'https://www.w3schools.com/', 'Expected URL: Popup');        
+    await I.closeCurrentTab();
+
+    const allHandlesAfterPopupClosed = await I.grabAllWindowHandles();    
+    assert.equal(allHandlesAfterPopupClosed.length, 1, 'Single Window');
+    const currentWindowHandle = await I.grabCurrentWindowHandle();    
+    assert.equal(currentWindowHandle, allHandlesAfterPopup[0], 'Expected Window: Main Window');
+
+}).tag('@ProofOfConcept').tag('@grabAllWindowHandles').tag('@grabCurrentWindowHandle').tag('@switchToWindow');
+```
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
index 64edd832..9915d6b0 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -16,7 +16,7 @@
 * [Puppeteer] Fixed `UnhandledPromiseRejectionWarning: "Execution context was destroyed...` by @adrielcodeco
 * [WebDriver] Keep browser window dimensions when starting a new session by @spiroid
 * Replace Ghekrin plceholders with values in files that combine a scenerio outline and table by @medtoure18.
-* Added Documentation to [locate elements in React Native](https://codecept.io/react-native-locators) apps. By @DimGun.
+* Added Documentation to [locate elements in React Native](https://codecept.io/mobile-react-native-locators) apps. By @DimGun.
 * Adding optional `path` parameter to `bdd:snippets` command to append snippets to a specific file. By @cthorsen31.
 * Added optional `output` parameter to `def` command by @LukoyanovE.
 * [Puppeteer] Added `grabDataFromPerformanceTiming` by @PeterNgTr.
diff --git a/lib/codecept.js b/lib/codecept.js
index 08972f39..71201315 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -161,17 +161,20 @@ class Codecept {
       }
       mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test);
     }
-    event.emit(event.all.before, this);
-    mocha.run(() => {
-      const done = () => {
-        event.emit(event.all.result, this);
-        event.emit(event.all.after, this);
-      };
+    const done = () => {
+      event.emit(event.all.result, this);
+      event.emit(event.all.after, this);
+    };
+
+    try {
+      event.emit(event.all.before, this);
+      mocha.run(() => this.teardown(done));
+    } catch (e) {
       this.teardown(done);
-    });
+      throw new Error(e);
+    }
   }
 
-
   static version() {
     return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version;
   }
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index 64570495..dd97c524 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -66,7 +66,9 @@ module.exports = function (selectedRuns, options) {
   runHook(config.bootstrapAll, done, 'bootstrapAll');
 
   if (options.config) { // update paths to config path
-    config.tests = path.resolve(testRoot, config.tests);
+    if (config.tests) {
+      config.tests = path.resolve(testRoot, config.tests);
+    }
     if (config.gherkin && config.gherkin.features) {
       config.gherkin.features = path.resolve(testRoot, config.gherkin.features);
     }
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 1ead21c0..4e0ceb55 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -116,7 +116,7 @@ class Appium extends Webdriver {
     const defaults = {
       // webdriverio defaults
       protocol: 'http',
-      host: '0.0.0.0',
+      hostname: '0.0.0.0', // webdriverio specs
       port: 4723,
       path: '/wd/hub',
 
@@ -146,6 +146,8 @@ class Appium extends Webdriver {
     config.capabilities.platformName = config.platform || config.capabilities.platformName;
     config.waitForTimeout /= 1000; // convert to seconds
 
+    // [CodeceptJS compatible] transform host to hostname
+    config.hostname = config.host || config.hostname;
 
     if (!config.app && config.capabilities.browserName) {
       this.isWeb = true;
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 06905312..90c2d317 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -1581,6 +1581,47 @@ class WebDriver extends Helper {
     await browser.buttonUp(0);
   }
 
+  /**
+   * Get all Window Handles.
+   * Useful for referencing a specific handle when calling `I.switchToWindow(handle)`
+   *
+   * ```js
+   * const windows = await I.grabAllWindowHandles();
+   * ```
+   */
+  async grabAllWindowHandles() {
+    return this.browser.getWindowHandles();
+  }
+
+  /**
+   * Get the current Window Handle.
+   * Useful for referencing it when calling `I.switchToWindow(handle)`
+   *
+   * ```js
+   * const window = await I.grabCurrentWindowHandle();
+   * ```
+   */
+  async grabCurrentWindowHandle() {
+    return this.browser.getWindowHandle();
+  }
+
+  /**
+   * Switch to the window with a specified handle.
+   *
+   * ```js
+   * const windows = await I.grabAllWindowHandles();
+   * // ... do something
+   * await I.switchToWindow( windows[0] );
+   *
+   * const window = await.grabCurrentWindowHandle();
+   * // ... do something
+   * await I.switchToWindow( window );
+   * ```
+   */
+  async switchToWindow(window) {
+    await this.browser.switchToWindow(window);
+  }
+
 
   /**
    * Close all tabs except for the current one.
diff --git a/lib/history.js b/lib/history.js
new file mode 100644
index 00000000..17e71b94
--- /dev/null
+++ b/lib/history.js
@@ -0,0 +1,39 @@
+const fs = require('fs');
+const path = require('path');
+
+const output = require('./output');
+const colors = require('chalk');
+
+/**
+ * REPL history records REPL commands and stores them in
+ * a file (~history) when session ends.
+ */
+class ReplHistory {
+  constructor() {
+    this.commands = [];
+  }
+
+  push(cmd) {
+    this.commands.push(cmd);
+  }
+
+  pop() {
+    this.commands.pop();
+  }
+
+  save() {
+    if (this.commands.length === 0) {
+      return;
+    }
+
+    const historyFile = path.join(global.output_dir, 'cli-history');
+    const commandSnippet = `\n\n<<< Recorded commands on ${new Date()}\n${this.commands.join('\n')}`;
+    fs.appendFileSync(historyFile, commandSnippet);
+
+    output.print(colors.yellow(` Commands have been saved to ${historyFile}`));
+
+    this.commands = [];
+  }
+}
+
+module.exports = new ReplHistory();
diff --git a/lib/hooks.js b/lib/hooks.js
index 66d51af5..354da999 100644
--- a/lib/hooks.js
+++ b/lib/hooks.js
@@ -1,6 +1,7 @@
 const getParamNames = require('./utils').getParamNames;
 const fsPath = require('path');
 const fileExists = require('./utils').fileExists;
+const output = require('./output');
 
 module.exports = function (hook, done, stage) {
   stage = stage || 'bootstrap';
@@ -39,6 +40,9 @@ function loadCustomHook(module) {
 
 function callSync(callable, done) {
   if (isAsync(callable)) {
+    callAsync(callable, done);
+    if (!hasArguments(callable)) done();
+  } else if (hasArguments(callable)) {
     callable(done);
   } else {
     callable();
@@ -46,7 +50,20 @@ function callSync(callable, done) {
   }
 }
 
-function isAsync(fn) {
+function callAsync(callable, done = undefined) {
+  const called = done ? callable(done) : callable();
+  called.catch((err) => {
+    output.print('');
+    output.error(err.message);
+    output.print('');
+    output.print(output.colors.grey(err.stack.replace(err.message, '')));
+    process.exit(1);
+  });
+}
+
+const isAsync = fn => fn.constructor.name === 'AsyncFunction';
+
+function hasArguments(fn) {
   const params = getParamNames(fn);
   return params && params.length;
 }
diff --git a/lib/pause.js b/lib/pause.js
index 70f3e27e..2efd17cb 100644
--- a/lib/pause.js
+++ b/lib/pause.js
@@ -1,4 +1,5 @@
 const container = require('./container');
+const history = require('./history');
 const store = require('./store');
 const recorder = require('./recorder');
 const event = require('./event');
@@ -58,18 +59,27 @@ function parseInput(cmd) {
     finish();
     recorder.session.restore();
     rl.close();
+    history.save();
     return nextStep();
   }
   store.debugMode = true;
   try {
     const locate = global.locate; // enable locate in this context
     const I = container.support('I');
-    eval(`I.${cmd}`); // eslint-disable-line no-eval
+
+    const fullCommand = `I.${cmd}`;
+    eval(fullCommand); // eslint-disable-line no-eval
+
+    history.push(fullCommand); // add command to history when successful
   } catch (err) {
     output.print(output.styles.error(' ERROR '), err.message);
   }
   recorder.session.catch((err) => {
     const msg = err.cliMessage ? err.cliMessage() : err.message;
+
+    // pop latest command from history because it failed
+    history.pop();
+
     return output.print(output.styles.error(' FAIL '), msg);
   });
   recorder.add('ask for next step', askForStep);
diff --git a/lib/scenario.js b/lib/scenario.js
index 337b3fa5..4b93e766 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -95,21 +95,32 @@ module.exports.test = (test) => {
  */
 module.exports.injected = function (fn, suite, hookName) {
   return function (done) {
-    recorder.errHandler((err) => {
+    const errHandler = (err) => {
       recorder.session.start('teardown');
       recorder.cleanAsyncErr();
       event.emit(event.test.failed, suite, err);
       if (hookName === 'after') event.emit(event.test.after, suite);
       if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
       recorder.add(() => done(err));
+    };
+
+    recorder.errHandler((err) => {
+      errHandler(err);
     });
 
     if (!fn) throw new Error('fn is not defined');
 
+    event.emit(event.hook.started, suite);
+    if (!recorder.isRunning()) {
+      recorder.start();
+      recorder.errHandler((err) => {
+        errHandler(err);
+      });
+    }
+
+    this.test.body = fn.toString();
+
     if (isAsyncFunction(fn)) {
-      event.emit(event.hook.started, suite);
-      recorder.startUnlessRunning();
-      this.test.body = fn.toString();
       fn.apply(this, getInjectedArguments(fn)).then(() => {
         recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
         recorder.add(`finish ${hookName} hook`, () => done());
@@ -118,28 +129,23 @@ module.exports.injected = function (fn, suite, hookName) {
         recorder.throw(e);
         recorder.catch((e) => {
           const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
-          recorder.session.start('teardown');
-          recorder.cleanAsyncErr();
-          event.emit(event.test.failed, suite, err);
-          if (hookName === 'after') event.emit(event.test.after, suite);
-          if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
-          recorder.add(() => done(err));
+          errHandler(err);
         });
       });
       return;
     }
 
     try {
-      event.emit(event.hook.started, suite);
-      recorder.startUnlessRunning();
-      this.test.body = fn.toString();
-      const res = fn.apply(this, getInjectedArguments(fn));
-    } catch (err) {
-      recorder.throw(err);
-    } finally {
+      fn.apply(this, getInjectedArguments(fn));
       recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
       recorder.add(`finish ${hookName} hook`, () => done());
       recorder.catch();
+    } catch (err) {
+      recorder.throw(err);
+      recorder.catch((e) => {
+        const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
+        errHandler(err);
+      });
     }
   };
 };
diff --git a/translations/de-DE.js b/translations/de-DE.js
new file mode 100644
index 00000000..e44a39e9
--- /dev/null
+++ b/translations/de-DE.js
@@ -0,0 +1,69 @@
+module.exports = {
+  I: 'Ich',
+  actions: {
+    amOutsideAngularApp: 'befinde_mich_außerhalb_der_angular_app',
+    amInsideAngularApp: 'bedinde_mich_innerhalb_der_angular_app',
+    waitForElement: 'warte_auf_element',
+    waitForClickable: 'warte_bis_clickbar',
+    waitForVisible: 'warte_bis_sichtbar',
+    waitForEnabled: 'warte_bis_enabled',
+    waitForInvisible: 'warte_bis_nicht_mehr_sichtbar',
+    waitInUrl: 'warte_auf_url',
+    waitForText: 'warte_auf_text',
+    moveTo: 'bewege_den_cursor_zu',
+    refresh: 'lade_die_seite_erneut',
+    refreshPage: 'lade_die_seite_erneut',
+    haveModule: 'habe_modul',
+    resetModule: 'setze_modul_zurück',
+    amOnPage: 'bin_auf_seite',
+    click: 'clicke',
+    doubleClick: 'doppelclicke',
+    see: 'sehe',
+    dontSee: 'sehe_nicht',
+    selectOption: 'wähle_option',
+    fillField: 'fülle_das_feld',
+    pressKey: 'drücke',
+    triggerMouseEvent: 'triggere_ein_mouseevent',
+    attachFile: 'füge_datei_hinzu',
+    seeInField: 'sehe_in_feld',
+    dontSeeInField: 'sehe_nicht_in_feld',
+    appendField: 'hänge_an_in_feld',
+    checkOption: 'checke_das_optionsfeld',
+    seeCheckboxIsChecked: 'sehe_dass_option_gecheckt_ist',
+    dontSeeCheckboxIsChecked: 'sehe_nicht_dass_option_gecheckt_ist',
+    grabTextFrom: 'hole_text_aus',
+    grabValueFrom: 'hole_wert_aus',
+    grabAttributeFrom: 'hole_attribut_aus',
+    seeInTitle: 'sehe_in_seitentitel',
+    dontSeeInTitle: 'sehe_nicht_in_seitentitel',
+    grabTitle: 'hole_seitentitel',
+    seeElement: 'sehe_element',
+    dontSeeElement: 'sehe_nicht_element',
+    seeInSource: 'sehe_im_html',
+    dontSeeInSource: 'sehe_nicht_im_html',
+    executeScript: 'führe_javascript_aus',
+    executeAsyncScript: 'führe_asynchrones_javascript_aus',
+    seeInCurrentUrl: 'sehe_in_aktueller_url',
+    dontSeeInCurrentUrl: 'sehe_nicht_in_aktueller_url',
+    seeCurrentUrlEquals: 'sehe_dass_url_gleich',
+    dontSeeCurrentUrlEquals: 'sehe_dass_url_ungleich',
+    saveScreenshot: 'speichere_screenshot',
+    setCookie: 'setze_cookie',
+    clearCookie: 'lösche_cookie',
+    seeCookie: 'sehe_cookie',
+    dontSeeCookie: 'sehe_nicht_cookie',
+    grabCookie: 'hole_cookie',
+    resizeWindow: 'ändere_fenstergröße',
+    wait: 'warte',
+    haveHeader: 'verwende_http_header',
+    clearField: 'lösche_feld',
+    dontSeeElementInDOM: 'sehe_nicht_element_in_dom',
+    moveCursorTo: 'bewege_den_cursor_zu',
+    scrollTo: 'scrolle_zu',
+    sendGetRequest: 'mache_einen_get_request',
+    sendPutRequest: 'mache_einen_put_request',
+    sendDeleteRequest: 'mache_einen_delete_request',
+    sendPostRequest: 'mache_einen_post_request',
+    switchTo: 'wechlse_in_iframe',
+  },
+};
diff --git a/translations/index.js b/translations/index.js
index f5285e69..0d5df5a0 100644
--- a/translations/index.js
+++ b/translations/index.js
@@ -5,3 +5,4 @@ exports['pl-PL'] = require('./pl-PL');
 exports['zh-CN'] = require('./zh-CN');
 exports['zh-TW'] = require('./zh-TW');
 exports['ja-JP'] = require('./ja-JP');
+exports['de-DE'] = require('./de-DE');


Test patch:
diff --git a/test/data/sandbox/configs/bootstrap/failed_test.js b/test/data/sandbox/configs/bootstrap/failed_test.js
new file mode 100644
index 00000000..fc45269f
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/failed_test.js
@@ -0,0 +1,7 @@
+Feature('Filesystem').tag('main');
+
+Scenario('check current dir', (I) => {
+  I.amInPath('.');
+  I.say('hello world');
+  I.seeFile('unknown.js');
+}).tag('slow').tag('@important');
diff --git a/test/data/sandbox/configs/bootstrap/fs_test.js b/test/data/sandbox/configs/bootstrap/fs_test.js
new file mode 100644
index 00000000..ac8b3dc3
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/fs_test.js
@@ -0,0 +1,7 @@
+Feature('Filesystem').tag('main');
+
+Scenario('check current dir', (I) => {
+  I.amInPath('.');
+  I.say('hello world');
+  I.seeFile('fs_test.js');
+}).tag('slow').tag('@important');
diff --git a/test/data/sandbox/configs/bootstrap/invalid_require_test.js b/test/data/sandbox/configs/bootstrap/invalid_require_test.js
new file mode 100644
index 00000000..131bb1ba
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/invalid_require_test.js
@@ -0,0 +1,9 @@
+require('invalidRequire');
+
+Feature('Filesystem');
+
+Scenario('check current dir', (I) => {
+  I.amInPath('.');
+  I.say('hello world');
+  I.seeFile('codecept.json');
+});
diff --git a/test/data/sandbox/codecept.bootstrap.obj.json b/test/data/sandbox/configs/bootstrap/obj.json
similarity index 59%
rename from test/data/sandbox/codecept.bootstrap.obj.json
rename to test/data/sandbox/configs/bootstrap/obj.json
index bde7f03d..7d20efd3 100644
--- a/test/data/sandbox/codecept.bootstrap.obj.json
+++ b/test/data/sandbox/configs/bootstrap/obj.json
@@ -1,13 +1,13 @@
 {
-  "tests": "./*_test.js",
+  "tests": "./fs_test.js",
   "timeout": 10000,
   "output": "./output",
   "helpers": {
     "FileSystem": {}
   },
   "include": {},
-  "bootstrap": "hooks.js",
-  "teardown": "hooks.js",
+  "bootstrap": "../../hooks.js",
+  "teardown": "../../hooks.js",
   "mocha": {},
   "name": "sandbox"
 }
\ No newline at end of file
diff --git a/test/data/sandbox/codecept.bootstrap.sync.json b/test/data/sandbox/configs/bootstrap/sync.json
similarity index 66%
rename from test/data/sandbox/codecept.bootstrap.sync.json
rename to test/data/sandbox/configs/bootstrap/sync.json
index 70c34bc0..dcc85ff8 100644
--- a/test/data/sandbox/codecept.bootstrap.sync.json
+++ b/test/data/sandbox/configs/bootstrap/sync.json
@@ -1,12 +1,12 @@
 {
-  "tests": "./*_test.js",
+  "tests": "./fs_test.js",
   "timeout": 10000,
   "output": "./output",
   "helpers": {
     "FileSystem": {}
   },
   "include": {},
-  "bootstrap": "bootstrap.sync.js",
+  "bootstrap": "../../bootstrap.sync.js",
   "mocha": {},
   "name": "sandbox"
 }
\ No newline at end of file
diff --git a/test/data/sandbox/configs/bootstrap/with.args.async.func.js b/test/data/sandbox/configs/bootstrap/with.args.async.func.js
new file mode 100644
index 00000000..4e759ce2
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/with.args.async.func.js
@@ -0,0 +1,15 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async (done) => {
+    console.log('I am bootstrap');
+    done();
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/with.args.bootstrap.teardown.js b/test/data/sandbox/configs/bootstrap/with.args.bootstrap.teardown.js
new file mode 100644
index 00000000..d6494a28
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/with.args.bootstrap.teardown.js
@@ -0,0 +1,19 @@
+exports.config = {
+  tests: './invalid_require_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async (done) => {
+    console.log('I am bootstrap');
+    done();
+  },
+  teardown: (done) => {
+    console.log('I am teardown');
+    done();
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.async.func.js b/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.async.func.js
new file mode 100644
index 00000000..a11417b4
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.async.func.js
@@ -0,0 +1,15 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async (done) => {
+    console.log('I am bootstrap');
+    throw new Error('Error from async bootstrap');
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.teardown.js b/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.teardown.js
new file mode 100644
index 00000000..50dd6321
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/with.args.failed.bootstrap.teardown.js
@@ -0,0 +1,19 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async (done) => {
+    console.log('I am bootstrap');
+    throw new Error('Error from async bootstrap');
+  },
+  teardown: (done) => {
+    console.log('I am teardown');
+    done();
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/without.args.async.func.js b/test/data/sandbox/configs/bootstrap/without.args.async.func.js
new file mode 100644
index 00000000..94cbf50e
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/without.args.async.func.js
@@ -0,0 +1,14 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async () => {
+    console.log('I am bootstrap');
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/without.args.bootstrap.teardown.js b/test/data/sandbox/configs/bootstrap/without.args.bootstrap.teardown.js
new file mode 100644
index 00000000..86de5da3
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/without.args.bootstrap.teardown.js
@@ -0,0 +1,17 @@
+exports.config = {
+  tests: './invalid_require.test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async () => {
+    console.log('I am bootstrap');
+  },
+  teardown: async () => {
+    console.log('I am teardown');
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.async.func.js b/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.async.func.js
new file mode 100644
index 00000000..e4b4b607
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.async.func.js
@@ -0,0 +1,15 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async () => {
+    console.log('I am bootstrap');
+    throw new Error('Error from async bootstrap');
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.teardown.js b/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.teardown.js
new file mode 100644
index 00000000..15d417ff
--- /dev/null
+++ b/test/data/sandbox/configs/bootstrap/without.args.failed.bootstrap.teardown.js
@@ -0,0 +1,18 @@
+exports.config = {
+  tests: './fs_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    FileSystem: {},
+  },
+  include: {},
+  bootstrap: async () => {
+    console.log('I am bootstrap');
+    throw new Error('Error from async bootstrap');
+  },
+  teardown: async () => {
+    console.log('I am teardown');
+  },
+  mocha: {},
+  name: 'sandbox',
+};
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 508c9d07..118afbc3 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -693,4 +693,41 @@ describe('WebDriver', function () {
       await wd.dontSeeCheckboxIsChecked('interesting');
     });
   });
+
+  describe('allow back and forth between handles: #grabAllWindowHandles #grabCurrentWindowHandle #switchToWindow', () => {
+    it('should open main page of configured site, open a popup, switch to main page, then switch to popup, close popup, and go back to main page', function* () {
+      yield wd.amOnPage('/');
+      const handleBeforePopup = yield wd.grabCurrentWindowHandle();
+      const urlBeforePopup = yield wd.grabCurrentUrl();
+
+      const allHandlesBeforePopup = yield wd.grabAllWindowHandles();
+      allHandlesBeforePopup.length.should.eql(1);
+
+      yield wd.executeScript(() => {
+        window.open('https://www.w3schools.com/', 'new window', 'toolbar=yes,scrollbars=yes,resizable=yes,width=400,height=400');
+      });
+
+      const allHandlesAfterPopup = yield wd.grabAllWindowHandles();
+      allHandlesAfterPopup.length.should.eql(2);
+
+      yield wd.switchToWindow(allHandlesAfterPopup[1]);
+      const urlAfterPopup = yield wd.grabCurrentUrl();
+      urlAfterPopup.should.eql('https://www.w3schools.com/');
+
+      handleBeforePopup.should.eql(allHandlesAfterPopup[0]);
+      yield wd.switchToWindow(handleBeforePopup);
+      const currentURL = yield wd.grabCurrentUrl();
+      currentURL.should.eql(urlBeforePopup);
+
+      yield wd.switchToWindow(allHandlesAfterPopup[1]);
+      const urlAfterSwitchBack = yield wd.grabCurrentUrl();
+      urlAfterSwitchBack.should.eql('https://www.w3schools.com/');
+      yield wd.closeCurrentTab();
+
+      const allHandlesAfterPopupClosed = yield wd.grabAllWindowHandles();
+      allHandlesAfterPopupClosed.length.should.eql(1);
+      const currentWindowHandle = yield wd.grabCurrentWindowHandle();
+      currentWindowHandle.should.eql(handleBeforePopup);
+    });
+  });
 });
diff --git a/test/runner/bootstrap_test.js b/test/runner/bootstrap_test.js
new file mode 100644
index 00000000..00b2b109
--- /dev/null
+++ b/test/runner/bootstrap_test.js
@@ -0,0 +1,203 @@
+const assert = require('assert');
+const path = require('path');
+const exec = require('child_process').exec;
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/bootstrap');
+const codecept_run = `${runner} run`;
+const codecept_run_config = (config, grep) => `${codecept_run} --config ${codecept_dir}/${config} ${grep ? `--grep ${grep}` : ''}`;
+const config_run_override = (config, override) => `${codecept_run} --config ${codecept_dir}/${config} --override '${JSON.stringify(override)}'`;
+
+
+describe('CodeceptJS Bootstrap and Teardown', () => {
+  // success
+  it('should run bootstrap', (done) => {
+    exec(codecept_run_config('sync.json', '@important'), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run teardown', (done) => {
+    exec(config_run_override('../../', { teardown: 'bootstrap.sync.js' }), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run async bootstrap', (done) => {
+    exec(config_run_override('../../', { bootstrap: 'bootstrap.async.js' }), (err, stdout, stderr) => {
+      stdout.should.include('Ready: 0');
+      stdout.should.include('Go: 1');
+      stdout.should.include('Filesystem'); // feature
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run bootstrap/teardown as object', (done) => {
+    exec(codecept_run_config('obj.json'), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('I am teardown');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run async bootstrap function without args', (done) => {
+    exec(codecept_run_config('without.args.async.func.js'), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run async bootstrap function with args', (done) => {
+    exec(codecept_run_config('with.args.async.func.js'), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      assert(!err);
+      done();
+    });
+  });
+
+  // failed test
+  it('should fail with code 1 when test failed and async bootstrap function without args', (done) => {
+    exec(config_run_override('without.args.async.func.js', { tests: './failed_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('✖ check current dir @slow @important');
+      done();
+    });
+  });
+
+  it('should fail with code 1 when test failed and async bootstrap function with args', (done) => {
+    exec(config_run_override('with.args.async.func.js', { tests: './failed_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('✖ check current dir @slow @important');
+      done();
+    });
+  });
+
+  // failed bootstrap
+  it('should fail with code 1 when async bootstrap function without args failed', (done) => {
+    exec(codecept_run_config('without.args.failed.bootstrap.async.func.js'), (err, stdout, stderr) => {
+      assert.equal(err.code, 1);
+      stdout.should.include('Error from async bootstrap');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      assert(err);
+      done();
+    });
+  });
+
+  it('should fail with code 1 when async bootstrap function with args failed', (done) => {
+    exec(codecept_run_config('with.args.failed.bootstrap.async.func.js'), (err, stdout, stderr) => {
+      assert.equal(err.code, 1);
+      stdout.should.include('Error from async bootstrap');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      assert(err);
+      done();
+    });
+  });
+
+  // failed in test file
+  it('should fail with code 1 when raise exceptin in the test file and async bootstrap function with args', (done) => {
+    exec(config_run_override('with.args.async.func.js', { tests: './invalid_require_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Cannot find module \'invalidRequire\'');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      done();
+    });
+  });
+
+  it('should fail with code 1 when raise exceptin in the test file and async bootstrap function without args', (done) => {
+    exec(config_run_override('without.args.async.func.js', { tests: './invalid_require_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Cannot find module \'invalidRequire\'');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      done();
+    });
+  });
+
+  // with teardown
+  it('should run async bootstrap/teardown with args', (done) => {
+    exec(config_run_override('with.args.bootstrap.teardown.js', { tests: './fs_test.js' }), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('I am teardown');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run async bootstrap/teardown without args', (done) => {
+    exec(config_run_override('without.args.bootstrap.teardown.js', { tests: './fs_test.js' }), (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('I am teardown');
+      assert(!err);
+      done();
+    });
+  });
+
+  // with teaedown - failed tests
+  it('should fail with code 1 when test failed and async bootstrap/teardown function with args', (done) => {
+    exec(config_run_override('with.args.bootstrap.teardown.js', { tests: './failed_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('✖ check current dir @slow @important');
+      stdout.should.include('I am teardown');
+      done();
+    });
+  });
+
+  it('should fail with code 1 when test failed and async bootstrap/teardown function without args', (done) => {
+    exec(config_run_override('without.args.bootstrap.teardown.js', { tests: './failed_test.js' }), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('I am bootstrap');
+      stdout.should.include('✖ check current dir @slow @important');
+      stdout.should.include('I am teardown');
+      done();
+    });
+  });
+
+  // with teardown and fail bootstrap - teardown not call
+  it('should fail with code 1 when async bootstrap with args failed and not call teardown', (done) => {
+    exec(codecept_run_config('with.args.failed.bootstrap.teardown.js'), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Error from async bootstrap');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      stdout.should.not.include('I am teardown');
+      done();
+    });
+  });
+
+  it('should fail with code 1 when async bootstrap without args failed and not call teardown', (done) => {
+    exec(codecept_run_config('without.args.failed.bootstrap.teardown.js'), (err, stdout, stderr) => {
+      assert(err);
+      assert.equal(err.code, 1);
+      stdout.should.include('Error from async bootstrap');
+      stdout.should.not.include('✔ check current dir @slow @important in 2ms');
+      stdout.should.not.include('I am teardown');
+      done();
+    });
+  });
+});
diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js
index a305bae0..b2deef17 100644
--- a/test/runner/codecept_test.js
+++ b/test/runner/codecept_test.js
@@ -168,34 +168,6 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
-  it('should run bootstrap', (done) => {
-    exec(codecept_run_config('codecept.bootstrap.sync.json'), (err, stdout, stderr) => {
-      stdout.should.include('Filesystem'); // feature
-      stdout.should.include('I am bootstrap');
-      assert(!err);
-      done();
-    });
-  });
-
-  it('should run teardown', (done) => {
-    exec(config_run_override({ teardown: 'bootstrap.sync.js' }), (err, stdout, stderr) => {
-      stdout.should.include('Filesystem'); // feature
-      stdout.should.include('I am bootstrap');
-      assert(!err);
-      done();
-    });
-  });
-
-  it('should run async bootstrap', (done) => {
-    exec(config_run_override({ bootstrap: 'bootstrap.async.js' }), (err, stdout, stderr) => {
-      stdout.should.include('Ready: 0');
-      stdout.should.include('Go: 1');
-      stdout.should.include('Filesystem'); // feature
-      assert(!err);
-      done();
-    });
-  });
-
   it('should run hooks', (done) => {
     exec(codecept_run_config('codecept.hooks.js'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
@@ -275,16 +247,6 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
-  it('should run bootstrap/teardown as object', (done) => {
-    exec(codecept_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');
-      assert(!err);
-      done();
-    });
-  });
-
   it('should run dynamic config', (done) => {
     exec(codecept_run_config('config.js'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature