Repository: Codeception/CodeceptJS
Pull request #859 (2018-01-06 12:28:24)

Custom output name for multiple browser run
Added possibility to use custom output name for directory

#858 


Patch:
diff --git a/.circleci/build.sh b/.circleci/build.sh
index b53ab223..08bcde68 100755
--- a/.circleci/build.sh
+++ b/.circleci/build.sh
@@ -10,6 +10,7 @@ docker-compose build \
   test-helpers \
   test-acceptance.webdriverio \
   test-acceptance.nightmare \
+  test-acceptance.puppeteer \
   json_server
 
 docker-compose pull \
diff --git a/.travis.yml b/.travis.yml
index 4c01db96..e4db4d2e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,6 +31,6 @@ before_script:
 - npm run json-server &
 - sleep 10
 script:
-- npm test
+- npm build && npm test
 - './node_modules/.bin/mocha test/rest'
 - './node_modules/.bin/mocha test/helper/${HELPER}_test.js'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a426f7c5..3ba01a0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,7 +36,8 @@ Execute test suite:
 
 ```sh
 mocha test/helper/WebDriverIO_test.js
-mocha test/helper/SeleniumWebdriver_test.js
+mocha test/helper/Puppeteer_test.js
+mocha test/helper/Nightmare_test.js
 ```
 
 Use `--grep` to execute tests only for changed parts.
@@ -79,7 +80,7 @@ Start JSON server to run tests:
 npm run json-server
 ```
 
-Edit a test at `tests/helper/REST_test.js` or `test/helper/ApiDataFactory_test.js`
+Edit a test at `test/rest/REST_test.js` or `test/rest/ApiDataFactory_test.js`
 
 ## Appium
 
@@ -110,6 +111,7 @@ mocha test/runner
 Please try to add corresponding testcase to runner or unit.
 
 ## Running tests in Dockerized environment
+
 Instead of manually running php, json_server and selenium for before tests you
 can use `docker-compose` to run those automatically.
 You can find `docker-compose.yml` file in `test` directory and run all commands
@@ -117,12 +119,14 @@ from this directory. Currently we provide following commands to run tests with
 respective dependencies:
 
 ### Run unit tests
-``` shell
+
+```sh
 docker-compose run --rm test-unit
 ```
 
 ### Run helper tests
-``` shell
+
+```sh
 docker-compose run --rm test-helpers
 
 # or pass path to helper test to run specific helper,
@@ -134,23 +138,28 @@ docker-compose run --rm test-helpers test/rest
 ```
 
 ### Run acceptance tests
-To that we provide two separate services respectively for WebDriverIO and
-Nightmare tests:
-``` shell
+
+To that we provide three separate services respectively for WebDriverIO, Nightmare and
+Puppeteer tests:
+
+```sh
 docker-compose run --rm test-acceptance.webdriverio
 docker-compose run --rm test-acceptance.nightmare
+docker-compose run --rm test-acceptance.puppeteer
 ```
 
 ### Running against specific Node version
-By default dockerized tests are run against node 6.9.5, you can run it against
+
+By default dockerized tests are run against node 8.9.1, you can run it against
 specific version as long as there is Docker container available for such
 version. To do that you need to build codecept's Docker image prior to running
 tests and pass `NODE_VERSION` as build argument.
 
-For example to prepare `test-helpers` containers based on node 8.7.0:
-``` shell
-docker-compose build --build-arg NODE_VERSION=8.7.0 test-helpers
+For example to prepare `test-helpers` containers based on node 9.4.0:
+
+```sh
+docker-compose build --build-arg NODE_VERSION=9.4.0 test-helpers
 ```
 
-And now every command based on `test-helpers` service will use node 8.7.0. The
+And now every command based on `test-helpers` service will use node 9.4.0. The
 same argument can be passed when building unit and acceptance tests services.
diff --git a/Dockerfile b/Dockerfile
index 282d3360..d3295f63 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,13 +10,43 @@ RUN apt-get update && \
       apt-get install -y libgtk2.0-0 libgconf-2-4 \
       libasound2 libxtst6 libxss1 libnss3 xvfb
 
+# Install latest chrome dev package.
+# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
+# installs, work.
+RUN apt-get update && apt-get install -y wget --no-install-recommends \
+    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
+    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
+    && apt-get update \
+    && apt-get install -y google-chrome-unstable \
+      --no-install-recommends \
+    && rm -rf /var/lib/apt/lists/* \
+    && apt-get purge --auto-remove -y curl \
+    && rm -rf /src/*.deb
+
+# Uncomment to skip the chromium download when installing puppeteer. If you do,
+# you'll need to launch puppeteer with:
+#     browser.launch({executablePath: 'google-chrome-unstable'})
+# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
+
+# Install puppeteer so it's available in the container.
+RUN yarn add puppeteer
+
+# Add pptr user.
+RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
+    && mkdir -p /home/pptruser/Downloads \
+    && chown -R pptruser:pptruser /home/pptruser \
+    && chown -R pptruser:pptruser /home/pptruser \
+    && chown -R pptruser:pptruser /node_modules
+
+RUN mkdir /codecept && chown -R pptruser:pptruser /codecept
+
 WORKDIR /tmp
 COPY package.json /tmp/
 
 # Install packages
 RUN npm install --loglevel=warn
 
-RUN mkdir /codecept
+#RUN mkdir /home/codecept
 WORKDIR /codecept
 
 COPY . /codecept
@@ -29,6 +59,9 @@ ENV CODECEPT_ARGS=""
 # Set HOST ENV variable for Selenium Server
 ENV HOST=selenium
 
+# Run user as non privileged.
+# USER pptruser
+
 # Set the entrypoint for Nightmare
 ENTRYPOINT ["docker/entrypoint"]
 
diff --git a/docs/acceptance.md b/docs/acceptance.md
index c1374b3b..19a9a653 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -249,16 +249,16 @@ within({frame: "#editor"}, () => {
 });
 ```
 
-Nested IFrames can be set by passing array *(WebDriverIO & Nightmare only)*:
+Nested IFrames can be set by passing array *(WebDriverIO, Nightmare & Puppeteer only)*:
 
 ```js
 within({frame: [".content", "#editor"]}, () => {
   I.see('Page');
 });
 ```
+
 ---
 
 ### done()
 
 CodeceptJS through helpers provides user friendly API to interact with a webpage. In this section we described using WebDriverIO helper which allows to control browser through Selenium WebDriver.
-
diff --git a/docs/api/helper.md b/docs/api/helper.md
index 3f62fe72..33fa4600 100644
--- a/docs/api/helper.md
+++ b/docs/api/helper.md
@@ -2,31 +2,39 @@
 
 **Parameters**
 
--   `config`  
+-   `config`
 
 ## _after
 
 Hook executed after each test
 
+## _afterStep
+
+Hook executed after each step
+
+**Parameters**
+
+-   `step`
+
 ## _afterSuite
 
 Hook executed after each suite
 
 **Parameters**
 
--   `suite`  
+-   `suite`
 
 ## _before
 
 Hook executed before each test.
 
-## _beforeSuite
+## _beforeStep
 
 Hook executed before each step
 
 **Parameters**
 
--   `step`  
+-   `step`
 
 ## _beforeSuite
 
@@ -34,15 +42,7 @@ Hook executed before each suite
 
 **Parameters**
 
--   `suite`  
-
-## _beforeSuite
-
-Hook executed after each step
-
-**Parameters**
-
--   `step`  
+-   `suite`
 
 ## _failed
 
@@ -50,7 +50,7 @@ Hook executed after each failed test
 
 **Parameters**
 
--   `test`  
+-   `test`
 
 ## _finishTest
 
@@ -58,7 +58,7 @@ Hook executed after all tests are executed
 
 **Parameters**
 
--   `suite`  
+-   `suite`
 
 ## _init
 
@@ -71,7 +71,7 @@ Executed in the very beginning of a test
 
 **Parameters**
 
--   `test`  
+-   `test`
 
 ## debug
 
@@ -79,7 +79,7 @@ Print debug message to console (outputs only in debug mode)
 
 **Parameters**
 
--   `msg`  
+-   `msg`
 
 ## helpers
 
diff --git a/docs/configuration.md b/docs/configuration.md
index f23d4e3d..27199476 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -7,7 +7,7 @@ After running `codeceptjs init` it should be saved in test root.
 Here is an overview of available options with their defaults:
 
 * **tests**: `"./*_test.js"` - pattern to locate tests
-* **include**: `{}` - actors and page objects to be registered in DI container and included in tests. Accepts objects and module `require ` paths
+* **include**: `{}` - actors and page objects to be registered in DI container and included in tests. Accepts objects and module `require` paths
 * **timeout**: `10000` - default tests timeout
 * **output**: `"./output"` - where to store failure screenshots, etc
 * **helpers**: `{}` - list of enabled helpers
diff --git a/docs/docker.md b/docs/docker.md
index 1a8b476d..02285935 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -80,6 +80,7 @@ docker build -t codeception/codeceptjs .
 ```
 
 ### Debugging
+
 To pass arguments to `codecept run` command inside docker image you can set
 `CODECEPT_ARGS` environment variable. For example to run your tests with debug
 and steps output:
diff --git a/docs/installation.md b/docs/installation.md
index 270da97a..1522eb60 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,6 +1,6 @@
 # Installation
 
-### Local
+## Local
 
 Use NPM install CodeceptJS:
 
@@ -38,7 +38,6 @@ To use it with Puppeteer install puppeteer package:
 npm install puppeteer --save-dev
 ```
 
-
 ## Meta Packages
 
 By default it doesn't install any backends like Webdriverio, Protractor, or Nightmare, so you need to install corresponding packages manually, or install one of the provided meta-packages:
diff --git a/docs/pageobjects.md b/docs/pageobjects.md
index 090b4de4..8464cac7 100644
--- a/docs/pageobjects.md
+++ b/docs/pageobjects.md
@@ -68,6 +68,7 @@ Scenario('login', (I, loginPage) => {
 ```
 
 Also you can use `async/await` inside PageObject:
+
 ```js
 'use strict';
 let I;
@@ -109,7 +110,6 @@ Scenario('login2', async (I, loginPage, basePage) => {
 });
 ```
 
-
 Also you can use generators inside a PageObject:
 
 ```js
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index 9be1895f..89630471 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -5,8 +5,8 @@ It operates over Google Chrome directly without requireing additional tools like
 
 CodeceptJS uses Puppeteer to improve end to end testing experience.
 
-1. No need to learn the syntax of a new tool, all drivers in CodeceptJS share the same API.
-2. CodeceptJS can locate elements by XPath.
+1.  No need to learn the syntax of a new tool, all drivers in CodeceptJS share the same API.
+2.  CodeceptJS can locate elements by XPath.
 
 Take a look at a sample test:
 
@@ -33,7 +33,6 @@ npm install -g codeceptjs-puppeteer
 Or see [alternative installation options](http://codecept.io/installation/)
 If you already have CodeceptJS project, just install `puppeteer` package and enable it in config.
 
-
 And a basic project initialized
 
 ```sh
@@ -68,7 +67,7 @@ Sometimes test may run faster than application gets rendered. In this case it is
 
 CodeceptJS test should be created with `gt` command:
 
-```
+```sh
 codeceptjs gt
 ```
 
@@ -154,15 +153,22 @@ I.see('0 items left', '.todo-count');
 
 CodeceptJS allows you to implement custom actions like `I.createTodo` or use **PageObjects**. Learn how to improve your tests in [PageObjects](http://codecept.io/pageobjects/) guide.
 
+`within` can also work with [iframes](/acceptance/#iframes)
+
+When running steps inside a within block will be shown with a shift:
+
+![within](http://codecept.io/images/within.png)
+
 ## Extending
 
 Puppeteer has a very [rich and flexible API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md). Sure, you can extend your test suites to use the methods listed there. CodeceptJS already prepares some objects for you and you can use them from your you helpers.
 
 Start with creating an `MyPuppeteer` helper using `generate:helper` or `gh` command:
 
-```
+```sh
 codeceptjs gh
 ```
+
 Then inside a Helper you can access `Puppeteer` helper of CodeceptJS.
 Let's say you want to create `I.renderPageToPdf` action. In this case you need to call `pdf` method of `page` object
 
diff --git a/docs/webapi/uncheckOption.mustache b/docs/webapi/uncheckOption.mustache
new file mode 100644
index 00000000..58f1f033
--- /dev/null
+++ b/docs/webapi/uncheckOption.mustache
@@ -0,0 +1,12 @@
+Unselects a checkbox or radio button.
+Element is located by label or name or CSS or XPath.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+I.uncheckOption('#agree');
+I.uncheckOption('I Agree to Terms and Conditions');
+I.uncheckOption('agree', '//form');
+```
+@param field checkbox located by label | name | CSS | XPath | strict locator
+@param context (optional) element located by CSS | XPath | strict locator
diff --git a/lib/codecept.js b/lib/codecept.js
index 4b58591d..b5433f46 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -20,7 +20,7 @@ class Codecept {
   constructor(config, opts) {
     this.config = Config.create(config);
     this.opts = opts;
-    this.testFiles = [];
+    this.testFiles = new Array(0);
   }
 
   /**
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index d2c48385..adaa647a 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -105,7 +105,13 @@ function runSuite(suite, suiteConf, browser) {
     overriddenConfig.helpers = replaceValue(overriddenConfig.helpers, key, browserConfig[key]);
   }
 
-  const outputDir = suite + JSON.stringify(browserConfig).replace(/[^\d\w]+/g, '_') + suiteId;
+  let outputDir = `${suite}_`;
+  if (browserConfig.outputName) {
+    outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
+  } else {
+    outputDir += JSON.stringify(browserConfig).replace(/[^\d\w]+/g, '_');
+  }
+  outputDir += `_${suiteId}`;
 
   // tweaking default output directories and for mochawesome
   overriddenConfig = replaceValue(overriddenConfig, 'output', path.join(config.output, outputDir));
diff --git a/lib/container.js b/lib/container.js
index 099d8cc3..eef98646 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -103,9 +103,15 @@ function createHelpers(config) {
   let helperModule;
   for (const helperName in config) {
     try {
-      const moduleName = config[helperName].require
-        ? path.resolve(global.codecept_dir, config[helperName].require) // custom helper
-        : `./helper/${helperName}`; // built-in helper
+      if (config[helperName].require) {
+        if (config[helperName].require.startsWith('.')) {
+          moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
+        } else {
+          moduleName = config[helperName].require; // plugin helper
+        }
+      } else {
+        moduleName = `./helper/${helperName}`; // built-in helper
+      }
       const HelperClass = require(moduleName);
       if (HelperClass._checkRequirements) {
         const requirements = HelperClass._checkRequirements();
diff --git a/lib/data/table.js b/lib/data/table.js
index 9b22873e..e72bd014 100644
--- a/lib/data/table.js
+++ b/lib/data/table.js
@@ -4,7 +4,7 @@
 class DataTable {
   constructor(array) {
     this.array = array;
-    this.rows = [];
+    this.rows = new Array(0);
   }
 
   add(array) {
diff --git a/lib/helper.js b/lib/helper.js
index 6b6f6b96..0eb45c70 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -51,63 +51,65 @@ class Helper {
    * Hook provides a test details
    * Executed in the very beginning of a test
    *
-   * @param test
+   * @param {*} test
    */
-  _test() {
+  _test(test) {
 
   }
 
   /**
    * Hook executed after each failed test
    *
-   * @param test
+   * @param {*} test
    */
-  _failed() {
+  _failed(test) {
 
   }
 
   /**
    * Hook executed before each step
    *
-   * @param step
+   * @param {*} step
    * @override
-   *
-   * _beforeStep()
    */
+  _beforeStep(step) {
+
+  }
 
   /**
    * Hook executed after each step
    *
-   * @param step
+   * @param {*} step
    * @override
-   *
-   * _afterStep()
    */
+  _afterStep(step) {
+
+  }
 
   /**
    * Hook executed before each suite
    *
-   * @param suite
+   * @param {*} suite
    */
-  _beforeSuite() {
+  _beforeSuite(suite) {
 
   }
 
   /**
    * Hook executed after each suite
    *
-   * @param suite
+   * @param {*} suite
    */
-  _afterSuite() {
+  _afterSuite(suite) {
 
   }
 
   /**
    * Hook executed after all tests are executed
    *
-   * @param suite
+   * @param {*} suite
    */
-  _finishTest() {
+  _finishTest(suite) {
 
   }
 
diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js
index 847da2d3..bfb9adf2 100644
--- a/lib/helper/ApiDataFactory.js
+++ b/lib/helper/ApiDataFactory.js
@@ -174,8 +174,11 @@ class ApiDataFactory extends Helper {
 
     // clean up all created items
     for (const factoryName in this.created) {
-      this.debug(`Deleting ${this.created[factoryName].length} ${factoryName}(s)`);
-      this.created[factoryName].forEach(id => promises.push(this._requestDelete(factoryName, id)));
+      const createdItems = this.created[factoryName];
+      this.debug(`Deleting ${createdItems.length} ${factoryName}(s)`);
+      for (const id in createdItems) {
+        promises.push(this._requestDelete(factoryName, createdItems[id]));
+      }
     }
 
     return Promise.all(promises);
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index 60a4dc29..d6634a72 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -11,9 +11,12 @@ const {
 } = require('../utils');
 const path = require('path');
 const ElementNotFound = require('./errors/ElementNotFound');
+const Popup = require('./extras/Popup');
 
 const puppeteer = requireg('puppeteer');
 
+const popupStore = new Popup();
+
 /**
  * Uses [Google Chrome's Puppeteer](https://github.com/GoogleChrome/puppeteer) library to run tests inside headless Chrome.
  * Browser control is executed via DevTools without Selenium.
@@ -53,6 +56,7 @@ class Puppeteer extends Helper {
       uniqueScreenshotNames: false,
       restart: true,
       show: false,
+      defaultPopupAction: 'accept',
     };
 
     this.isRunning = false;
@@ -60,6 +64,7 @@ class Puppeteer extends Helper {
     // override defaults with config
     Object.assign(this.options, config);
     this.puppeteerOptions = Object.assign({ headless: !this.options.show }, this.options.chrome);
+    popupStore.defaultAction = this.options.defaultPopupAction;
   }
 
   static _config() {
@@ -95,6 +100,113 @@ class Puppeteer extends Helper {
     return this._stopBrowser();
   }
 
+  /**
+   * Set the automatic popup response to Accept.
+   * This must be set before a popup is triggered.
+   *
+   * ```js
+   * I.amAcceptingPopups();
+   * I.click('#triggerPopup');
+   * I.acceptPopup();
+   * ```
+   */
+  amAcceptingPopups() {
+    popupStore.actionType = 'accept';
+  }
+
+  /**
+   * Accepts the active JavaScript native popup window, as created by window.alert|window.confirm|window.prompt.
+   * Don't confuse popups with modal windows, as created by [various
+   * libraries](http://jster.net/category/windows-modals-popups).
+   */
+  acceptPopup() {
+    popupStore.assertPopupActionType('accept');
+  }
+
+  /**
+   * Set the automatic popup response to Cancel/Dismiss.
+   * This must be set before a popup is triggered.
+   *
+   * ```js
+   * I.amCancellingPopups();
+   * I.click('#triggerPopup');
+   * I.cancelPopup();
+   * ```
+   */
+  amCancellingPopups() {
+    popupStore.actionType = 'cancel';
+  }
+
+  /**
+   * Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
+   */
+  cancelPopup() {
+    popupStore.assertPopupActionType('cancel');
+  }
+
+  /**
+   * Checks that the active JavaScript popup, as created by `window.alert|window.confirm|window.prompt`, contains the
+   * given string.
+   */
+  async seeInPopup(text) {
+    popupStore.assertPopupVisible();
+    const popupText = await popupStore.popup.message();
+    stringIncludes('text in popup').assert(text, popupText);
+  }
+
+  /**
+   * Set current page
+   * @param {object} page page to set
+   */
+  _setPage(page) {
+    this._addPopupListener(page);
+    this.page = page;
+  }
+
+  /**
+   * Add the 'dialog' event listener to a page
+   * @page {Puppeteer.Page}
+   *
+   * The popup listener handles the dialog with the predefined action when it appears on the page.
+   * It also saves a reference to the object which is used in seeInPopup.
+   */
+  _addPopupListener(page) {
+    if (!page) {
+      return;
+    }
+    page.on('dialog', async (dialog) => {
+      popupStore.popup = dialog;
+      const action = popupStore.actionType || this.options.defaultPopupAction;
+      await this._waitForAction();
+
+      switch (action) {
+        case 'accept':
+          return dialog.accept();
+
+        case 'cancel':
+          return dialog.dismiss();
+
+        default: {
+          throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted');
+        }
+      }
+    });
+  }
+
+  /**
+   * Grab the text within the popup. If no popup is visible then it will return null
+   *
+   * ```js
+   * await I.grabPopupText();
+   * ```
+   */
+  async grabPopupText() {
+    if (popupStore.popup) {
+      return popupStore.popup.message();
+    }
+    return null;
+  }
+
   async _startBrowser() {
     this.browser = await puppeteer.launch(this.puppeteerOptions);
     this.browser.on('targetcreated', (target) => {
@@ -110,7 +222,7 @@ class Puppeteer extends Helper {
       this.debugSection('Url', target.url());
     });
 
-    this.page = await this.browser.newPage();
+    this._setPage(await this.browser.newPage());
 
     if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
       const dimensions = this.options.windowSize.split('x');
@@ -120,18 +232,46 @@ class Puppeteer extends Helper {
 
   async _stopBrowser() {
     this.withinLocator = null;
-    this.page = null;
+    this._setPage(null);
     this.context = null;
+    popupStore.clear();
     await this.browser.close();
   }
 
+  async _evaluateHandeInContext(...args) {
+    let context = await this._getContext();
+
+    if (context.constructor.name === 'Frame') {
+      // Currently there is no evalateHandle for the Frame object
+      // https://github.com/GoogleChrome/puppeteer/issues/1051
+      context = await context.executionContext();
+    }
+
+    return context.evaluateHandle(...args);
+  }
+
+
   async _withinBegin(locator) {
     if (this.withinLocator) {
       throw new Error('Can\'t start within block inside another within block');
     }
+
+    const frame = isFrameLocator(locator);
+
+    if (frame) {
+      if (Array.isArray(frame)) {
+        return this.switchTo(null)
+          .then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()));
+      }
+      await this.switchTo(locator);
+      this.withinLocator = new Locator(locator);
+      return;
+    }
+
     const els = await this._locate(locator);
     assertElementExists(els, locator);
     this.context = els[0];
+
     this.withinLocator = new Locator(locator);
   }
 
@@ -251,7 +391,7 @@ class Puppeteer extends Helper {
     const pages = await this.browser.pages();
     const index = pages.indexOf(this.page);
     this.withinLocator = null;
-    this.page = pages[(index + num) % pages.length];
+    this._setPage(pages[(index + num) % pages.length]);
     await this.page.bringToFront();
     return this._waitForAction();
   }
@@ -268,7 +408,7 @@ class Puppeteer extends Helper {
     const pages = await this.browser.pages();
     const index = pages.indexOf(this.page);
     this.withinLocator = null;
-    this.page = pages[(index - num) % pages.length];
+    this._setPage(pages[(index - num) % pages.length]);
     await this.page.bringToFront();
     return this._waitForAction();
   }
@@ -281,7 +421,9 @@ class Puppeteer extends Helper {
    * ```
    */
   async closeCurrentTab() {
-    return Promise.all([this.switchToPreviousTab(), this.page.close()]);
+    const oldPage = this.page;
+    await this.switchToPreviousTab();
+    return oldPage.close();
   }
 
   /**
@@ -292,7 +434,7 @@ class Puppeteer extends Helper {
    * ```
    */
   async openNewTab() {
-    this.page = await this.browser.newPage();
+    this._setPage(await this.browser.newPage());
   }
 
   /**
@@ -396,11 +538,12 @@ class Puppeteer extends Helper {
     // await el.focus();
     const tag = await el.getProperty('tagName').then(el => el.jsonValue());
     const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
+
     if (tag === 'TEXTAREA' || editable) {
-      await this.page.evaluateHandle(el => el.innerHTML = '', el);
+      await this._evaluateHandeInContext(el => el.innerHTML = '', el);
     }
     if (tag === 'INPUT') {
-      await this.page.evaluateHandle(el => el.value = '', el);
+      await this._evaluateHandeInContext(el => el.value = '', el);
     }
     await el.type(value, { delay: 10 });
     return this._waitForAction();
@@ -472,15 +615,15 @@ class Puppeteer extends Helper {
       const opt = xpathLocator.literal(option[key]);
       let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
       if (optEl.length) {
-        this.page.evaluateHandle(el => el.selected = true, optEl[0]);
+        this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
         continue;
       }
       optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
       if (optEl.length) {
-        this.page.evaluateHandle(el => el.selected = true, optEl[0]);
+        this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
       }
     }
-    await this.page.evaluateHandle((element) => {
+    await this._evaluateHandeInContext((element) => {
       element.dispatchEvent(new Event('input', { bubbles: true }));
       element.dispatchEvent(new Event('change', { bubbles: true }));
     }, el);
@@ -516,7 +659,7 @@ class Puppeteer extends Helper {
    * {{> ../webapi/dontSeeCurrentUrlEquals }}
    */
   async dontSeeCurrentUrlEquals(url) {
-    const currentUrl = this.page.url();
+    const currentUrl = await this.page.url();
     urlEquals(this.options.url).negate(url, currentUrl);
   }
 
@@ -530,7 +673,7 @@ class Puppeteer extends Helper {
   /**
    * {{> ../webapi/dontSee }}
    */
-  dontSee(text, context = null) {
+  async dontSee(text, context = null) {
     return proceedSee.call(this, 'negate', text, context);
   }
 
@@ -653,7 +796,7 @@ class Puppeteer extends Helper {
   async grabAttributeFrom(locator, attr) {
     const els = await this._locate(locator);
     assertElementExists(els, locator);
-    return this.page.evaluateHandle((el, attr) => el.getAttribute(attr), els[0], attr)
+    return this._evaluateHandeInContext((el, attr) => el.getAttribute(attr), els[0], attr)
       .then(t => t.jsonValue());
   }
 
@@ -697,16 +840,16 @@ class Puppeteer extends Helper {
   async waitForElement(locator, sec) {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
-    if (this.withinLocator) this.debug('waitForElement ignores `within` block');
 
     let waiter;
+    const context = await this._getContext();
     if (locator.isCSS()) {
-      waiter = this.page.waitForSelector(locator.simplify(), { timeout: waitTimeout });
+      waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout });
     } else {
-      waiter = this.page.waitForFunction($XPath, { timeout: waitTimeout }, null, locator.value);
+      waiter = context.waitForFunction($XPath, { timeout: waitTimeout }, null, locator.value);
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not present on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -717,19 +860,19 @@ class Puppeteer extends Helper {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
     const matcher = await this.context;
-    if (this.withinLocator) this.debug('waitForVisible ignores `within` block');
     let waiter;
+    const context = await this._getContext();
     if (locator.isCSS()) {
-      waiter = this.page.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true });
+      waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true });
     } else {
       const visibleFn = function (locator, $XPath) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).filter(el => el.offsetParent !== null).length > 0;
       };
-      waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not visible on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not visible on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -739,35 +882,43 @@ class Puppeteer extends Helper {
   async waitToHide(locator, sec) {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
-    if (this.withinLocator) this.debug('waitToHide ignores `within` block');
     let waiter;
+    const context = await this._getContext();
     if (locator.isCSS()) {
-      waiter = this.page.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
+      waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
     } else {
       const visibleFn = function (locator, $XPath) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).filter(el => el.offsetParent !== null).length === 0;
       };
-      waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not hidden after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
+  async _getContext() {
+    if (this.context && this.context.constructor.name === 'Frame') {
+      return this.context;
+    }
+    return this.page;
+  }
 
   /**
    * {{> ../webapi/waitForText }}
    */
   async waitForText(text, sec = null, context = null) {
-    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
-    if (this.withinLocator) this.debug('waitForVisible ignores `within` block');
+    const aSec = sec || this.options.waitForTimeout;
+    const waitTimeout = aSec * 1000;
     let waiter;
 
+    const contextObject = await this._getContext();
+
     if (context) {
       const locator = new Locator(context, 'css');
       if (locator.isCSS()) {
-        waiter = this.page.waitForFunction((locator, text) => {
+        waiter = contextObject.waitForFunction((locator, text) => {
           const el = document.querySelector(locator);
           if (!el) return false;
           return el.innerText.indexOf(text) > -1;
@@ -775,27 +926,67 @@ class Puppeteer extends Helper {
       }
 
       if (locator.isXPath()) {
-        waiter = this.page.waitForFunction((locator, text, $XPath) => {
+        waiter = contextObject.waitForFunction((locator, text, $XPath) => {
           const el = $XPath(null, locator);
           if (!el.length) return false;
           return el[0].innerText.indexOf(text) > -1;
         }, { timeout: waitTimeout }, locator.value, text, $XPath);
       }
     } else {
-      waiter = this.page.waitForFunction(text => document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text);
+      waiter = contextObject.waitForFunction(text => document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text);
     }
 
     return waiter.catch((err) => {
-      throw new Error(`Text "${text}" was not found on page after ${sec} sec`);
+      throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
+  /**
+   * Switches frame or in case of null locator reverts to parent.
+   * Appium: support only web testing
+   */
+  async switchTo(locator) {
+    if (!locator) {
+      this.context = await this.page.mainFrame().$('body');
+      return;
+    } else if (Number.isInteger(locator)) {
+      // Select by frame index of current context
+      const childFrames = this.context ? this.context.childFrames() : this.page.frames();
+
+      if (locator >= 0 && locator < childFrames.length) {
+        this.context = childFrames[locator];
+      } else {
+        throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath');
+      }
+      return;
+    }
+
+    // iframe by selector
+    const els = await this._locate(locator);
+    assertElementExists(els, locator);
+
+    const iframeName = await els[0].getProperty('name').then(el => el.jsonValue());
+    const iframeId = await els[0].getProperty('id').then(el => el.jsonValue());
+
+    const searchName = iframeName || iframeId; // Name takes precedence over id, because of puppeteer's Frame.name() function
+    const currentContext = await this._getContext();
+    const resFrame = await findFrame.call(this, currentContext, searchName, els[0]);
+
+    if (resFrame) {
+      this.context = resFrame;
+    } else {
+      this.context = els[0];
+    }
+  }
+
   /**
    * {{> ../webapi/waitUntil }}
    */
   async waitUntil(fn, sec = null) {
-    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
-    return this.page.waitForFunction(fn, { timeout: waitTimeout });
+    const aSec = sec || this.options.waitForTimeout;
+    const waitTimeout = aSec * 1000;
+    const context = await this._getContext();
+    return context.waitForFunction(fn, { timeout: waitTimeout });
   }
 
   /**
@@ -804,23 +995,23 @@ class Puppeteer extends Helper {
   async waitUntilExists(locator, sec) {
     const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
     locator = new Locator(locator, 'css');
-    if (this.withinLocator) this.debug('waitUntilExists ignores `within` block');
 
     let waiter;
+    const context = await this._getContext();
     if (locator.isCSS()) {
       const visibleFn = function (locator) {
         return document.querySelector(locator) === null;
       };
-      waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value);
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value);
     } else {
       const visibleFn = function (locator, $XPath) {
         eval($XPath); // eslint-disable-line no-eval
         return $XPath(null, locator).length === 0;
       };
-      waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -831,6 +1022,20 @@ class Puppeteer extends Helper {
 
 module.exports = Puppeteer;
 
+async function findFrame(context, locator, element) {
+  if (!context) {
+    return;
+  }
+  let frames = [];
+  if (typeof context.childFrames === 'function') {
+    frames = context.childFrames();
+  } else {
+    frames = this.page.frames();
+  }
+
+  return frames.find(frame => frame.name() === locator);
+}
+
 async function findElements(matcher, locator) {
   locator = new Locator(locator, 'css');
   if (!locator.isXPath()) return matcher.$$(locator.simplify());
@@ -839,15 +1044,11 @@ async function findElements(matcher, locator) {
   if (matcher.constructor.name === 'ElementHandle') {
     context = matcher;
   }
-  const arrayHandle = await matcher.executionContext().evaluateHandle($XPath, context, locator.value);
-  const properties = await arrayHandle.getProperties();
-  await arrayHandle.dispose();
-  const result = [];
-  for (const property of properties.values()) {
-    const elementHandle = property.asElement();
-    if (elementHandle) result.push(elementHandle);
+  if (matcher.constructor.name === 'Frame') {
+    context = matcher;
   }
-  return result;
+
+  return matcher.$x(locator.value, matcher);
 }
 
 async function proceedClick(locator, context = null, options = {}) {
@@ -890,8 +1091,13 @@ async function proceedSee(assertType, text, context) {
   let description;
   let allText;
   if (!context) {
-    // console.log('con', await this.context);
-    const el = await this.context;
+    let el = await this.context;
+
+    if (el && !el.getProperty) {
+      // Fallback to body
+      el = await this.context.$('body');
+    }
+
     allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
     description = 'web application';
   } else {
@@ -977,6 +1183,12 @@ async function proceedSeeInField(assertType, field, value) {
   return stringIncludes(`field by ${field}`)[assertType](value, fieldVal);
 }
 
+function isFrameLocator(locator) {
+  locator = new Locator(locator);
+  if (locator.isFrame()) return locator.value;
+  return false;
+}
+
 
 function assertElementExists(res, locator, prefix, suffix) {
   if (!res || res.length === 0) {
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index 9c35b4eb..d71cb1d2 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -1,6 +1,5 @@
 const Helper = require('../helper');
 const requireg = require('requireg');
-const co = require('co');
 
 let unirest = requireg('unirest');
 let headers = {};
@@ -13,20 +12,21 @@ let request;
  * ## Configuration
  *
  * * endpoint: API base URL
- * * timeout: timeout for requests in milliseconds. 10000 by default
+ * * timeout: timeout for requests in milliseconds. 10000ms by default
  * * defaultHeaders: a list of default headers
  * * resetHeaders: set to true to reset headers  between requests. Disabled by default
+ * * followRedirect: set to true to enable automatic redirect. Disabled by default
  *
  */
 class REST extends Helper {
   constructor(config) {
     super(config);
     unirest = requireg('unirest');
-
     this.options = {
       timeout: 10000,
       resetHeaders: false,
       defaultHeaders: {},
+      followRedirect: false,
       endpoint: '',
     };
     this.options = Object.assign(this.options, config);
@@ -41,6 +41,73 @@ class REST extends Helper {
     }
   }
 
+  /**
+   * Executes unirest request
+   *
+   * @param {*} request
+   */
+  async _executeRequest(request) {
+    if (headers && headers.auth) {
+      request.auth(headers.auth);
+    }
+    request.timeout(this.options.timeout);
+    request.followRedirect(this.options.followRedirect);
+    const response = await executeRequest(request);
+    this._cleanRequestHeaders();
+    return response;
+  }
+
+  /**
+   * Changes headers to default if reset headers option is true
+   *
+   */
+  _cleanRequestHeaders() {
+    if (this.options.resetHeaders) {
+      headers = Object.assign({}, this.options.defaultHeaders);
+    }
+  }
+
+  /**
+   * Generates url based on format sent (takes endpoint + url if latter lacks 'http')
+   *
+   * @param {*} url
+   */
+  _url(url) {
+    return url.indexOf('http') === -1 ? this.options.endpoint + url : url;
+  }
+
+  /**
+   * Set response auto-redirects ON
+   *
+   * ```js
+   * I.amFollowingRequestRedirects(); // To enable auto-redirects
+   * ```
+   *
+   */
+  amFollowingRequestRedirects() {
+    this.options.followRedirect = true;
+  }
+
+  /**
+   * Set response auto-redirects OFF
+   *
+   * ```js
+   * I.amNotFollowingRequestRedirects(); // To disable auto-redirects
+   * ```
+   *
+   */
+  amNotFollowingRequestRedirects() {
+    this.options.followRedirect = false;
+  }
+
+  /**
+   * Set timeout for the request
+   *
+   * ```js
+   * I.setRequestTimeout(10000); // In milliseconds
+   * ```
+   *
+   */
   setRequestTimeout(newTimeout) {
     this.options.timeout = newTimeout;
   }
@@ -64,14 +131,16 @@ class REST extends Helper {
     headers = Object.assign(headers, customHeaders);
   }
 
-  _cleanRequestHeaders() {
-    if (this.options.resetHeaders) {
-      headers = Object.assign({}, this.options.defaultHeaders);
-    }
-  }
-
-  _url(url) {
-    return this.options.endpoint + url;
+  /**
+   * Reset headers for the request to default state
+   *
+   * ```js
+   * I.resetRequestHeaders();
+   * ```
+   *
+   */
+  resetRequestHeaders() {
+    headers = Object.assign({}, this.options.defaultHeaders);
   }
 
   /**
@@ -82,7 +151,7 @@ class REST extends Helper {
    * ```
    *
    * @param {*} url
-   * @param object headers
+   * @param {object} headers
    */
   sendGetRequest(url, headers = {}) {
     request = unirest.get(this._url(url)).headers(headers);
@@ -98,9 +167,10 @@ class REST extends Helper {
    *
    * @param {*} url
    * @param {*} payload
-   * @param object headers
+   * @param {object} headers
    */
   sendPostRequest(url, payload = {}, headers = {}) {
+    payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
     request = unirest.post(this._url(url)).headers(headers).send(payload);
     return this._executeRequest(request);
   }
@@ -112,11 +182,12 @@ class REST extends Helper {
    * I.sendPatchRequest('/api/users.json', { "email": "user@user.com" });
    * ```
    *
-   * @param string url
-   * @param object payload
-   * @param object headers
+   * @param {string} url
+   * @param {object} payload
+   * @param {object} headers
    */
   sendPatchRequest(url, payload = {}, headers = {}) {
+    payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
     request = unirest.patch(this._url(url)).headers(headers).send(payload);
     return this._executeRequest(request);
   }
@@ -128,11 +199,12 @@ class REST extends Helper {
    * I.sendPutRequest('/api/users.json', { "email": "user@user.com" });
    * ```
    *
-   * @param string url
-   * @param object payload
-   * @param object headers
+   * @param {string} url
+   * @param {object} payload
+   * @param {object} headers
    */
   sendPutRequest(url, payload = {}, headers = {}) {
+    payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
     request = unirest.put(this._url(url)).headers(headers).send(payload);
     return this._executeRequest(request);
   }
@@ -145,37 +217,19 @@ class REST extends Helper {
    * ```
    *
    * @param {*} url
-   * @param {*} payload
+   * @param {object} headers
    */
   sendDeleteRequest(url, headers = {}) {
     request = unirest.delete(this._url(url)).headers(headers);
     return this._executeRequest(request);
   }
-
-  /**
-   * Executes unirest request
-   *
-   * @param {*} request
-   */
-  _executeRequest(request) {
-    if (headers && headers.auth) {
-      request.auth(headers.auth);
-    }
-    request.timeout(this.options.timeout);
-
-    return co(executeRequest(request)).then((response) => {
-      this._cleanRequestHeaders();
-      return response;
-    });
-  }
 }
-
 module.exports = REST;
 
-function* executeRequest(request) {
-  return new Promise(((resolve, reject) => {
+function executeRequest(request) {
+  return new Promise((resolve) => {
     request
       .headers(headers)
-      .end(response => resolve(response) || reject(response));
-  }));
+      .end(response => resolve(response));
+  });
 }
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index 0fda8066..e8b060dd 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -649,6 +649,24 @@ class WebDriverIO extends Helper {
     return this.browser[clickMethod](elem.ELEMENT);
   }
 
+  /**
+   * {{> ../webapi/uncheckOption }}
+   * Appium: not tested
+   */
+  async uncheckOption(field, context = null) {
+    const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick';
+    const locateFn = prepareLocateFn.call(this, context);
+
+    const res = await findCheckable.call(this, field, locateFn);
+
+    assertElementExists(res, field, 'Checkable');
+    const elem = res.value[0];
+
+    const isSelected = await this.browser.elementIdSelected(elem.ELEMENT);
+    if (!isSelected.value) return Promise.resolve(true);
+    return this.browser[clickMethod](elem.ELEMENT);
+  }
+
   /**
    * {{> ../webapi/grabTextFrom }}
    * Appium: support
@@ -901,6 +919,19 @@ class WebDriverIO extends Helper {
     return this.browser.log('browser').then(res => res.value);
   }
 
+  /**
+   * Get current URL from browser.
+   *
+   * ```js
+   * let url = yield I.grabBrowserUrl();
+   * console.log(`Current URL is [${url}]`);
+   * ```
+   */
+  async grabBrowserUrl() {
+    const res = await this.browser.url();
+    return res.value;
+  }
+
   /**
    * {{> ../webapi/dontSeeInSource }}
    * Appium: support
@@ -1250,6 +1281,18 @@ class WebDriverIO extends Helper {
     });
   }
 
+  /**
+   * Grab the text within the popup. If no popup is visible then it will return null
+   *
+   * ```js
+   * await I.grabPopupText();
+   * ```
+   */
+  async grabPopupText() {
+    return this.browser.alertText()
+      .catch(() => null); // Don't throw an error
+  }
+
   /**
    * {{> ../webapi/pressKey }}
    *
diff --git a/lib/helper/extras/Popup.js b/lib/helper/extras/Popup.js
new file mode 100644
index 00000000..ae6880ac
--- /dev/null
+++ b/lib/helper/extras/Popup.js
@@ -0,0 +1,68 @@
+
+/**
+ * Class to handle the interaction with the Dialog (Popup) Class from Puppeteer
+ */
+class Popup {
+  constructor(popup, defaultAction) {
+    this._popup = popup || {};
+    this._actionType = '';
+    this._defaultAction = defaultAction || '';
+  }
+
+  _assertValidActionType(action) {
+    if (['accept', 'cancel'].indexOf(action) === -1) {
+      throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted');
+    }
+  }
+
+  set defaultAction(action) {
+    this._assertValidActionType(action);
+    this._defaultAction = action;
+  }
+
+  get defaultAction() {
+    return this._defaultAction;
+  }
+
+  get popup() {
+    return this._popup;
+  }
+
+  set popup(popup) {
+    if (this._popup) {
+      console.error('Popup already exists and was not closed. Popups must always be closed by calling either I.acceptPopup() or I.cancelPopup()');
+    }
+    this._popup = popup;
+  }
+
+  get actionType() {
+    return this._actionType;
+  }
+
+  set actionType(action) {
+    this._assertValidActionType(action);
+    this._actionType = action;
+  }
+
+  clear() {
+    this._popup = null;
+    this._actionType = '';
+  }
+
+  assertPopupVisible() {
+    if (!this._popup) {
+      throw new Error('There is no Popup visible');
+    }
+  }
+
+  assertPopupActionType(type) {
+    this.assertPopupVisible();
+    const expectedAction = this._actionType || this._defaultAction;
+    if (expectedAction !== type) {
+      throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`);
+    }
+    this.clear();
+  }
+}
+
+module.exports = Popup;
diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js
index 5cc78459..a6266573 100644
--- a/lib/interfaces/bdd.js
+++ b/lib/interfaces/bdd.js
@@ -32,7 +32,7 @@ module.exports = function (suite) {
   suite.on('pre-require', (context, file, mocha) => {
     const common = require('mocha/lib/interfaces/common')(suites, context);
 
-    const addScenario = function (title, opts, fn) {
+    const addScenario = function (title, opts, fn, data) {
       const suite = suites[0];
 
       if (typeof opts === 'function' && !fn) {
@@ -139,10 +139,10 @@ module.exports = function (suite) {
     /**
      * Exclusive test-case.
      */
-    context.Scenario.only = function (title, data, opts, fn) {
+    context.Scenario.only = function (title, opts, fn, data) {
       const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`;
       mocha.grep(new RegExp(reString));
-      return addScenario(title, data, opts, fn);
+      return addScenario(title, opts, fn, data);
     };
 
     /**
diff --git a/lib/mocha_factory.js b/lib/mocha_factory.js
index efa5cf23..274704e2 100644
--- a/lib/mocha_factory.js
+++ b/lib/mocha_factory.js
@@ -34,10 +34,11 @@ class MochaFactory {
       });
     }
 
-    if (reporterOptions['codeceptjs-cli-reporter']) {
+    const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter');
+    if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
       Object.defineProperty(
         reporterOptions, 'codeceptjs/lib/reporter/cli',
-        Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter'),
+        attributes,
       );
       delete reporterOptions['codeceptjs-cli-reporter'];
     }
diff --git a/package.json b/package.json
index 53ea63a7..1bfd1d05 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
   },
   "repository": "Codeception/codeceptjs",
   "scripts": {
+    "build": "tsc -p ./",
     "json-server": "./node_modules/json-server/bin/index.js test/data/rest/db.json -p 8010 --watch -m test/data/rest/headers.js",
     "lint": "eslint bin/ examples/ lib/ test/ translations/",
     "lint-fix": "eslint bin/ examples/ lib/ test/ translations/ --fix",
@@ -46,7 +47,6 @@
     "requireg": "^0.1.5"
   },
   "devDependencies": {
-    "@types/commander": "^2.12.2",
     "@types/inquirer": "^0.0.35",
     "@types/node": "^8.5.1",
     "chai": "^3.4.1",
@@ -62,7 +62,7 @@
     "nightmare-upload": "^0.1.1",
     "nyc": "^11.4.1",
     "protractor": "^5.2.2",
-    "puppeteer": "^0.13.0",
+    "puppeteer": "^1.0.0",
     "rosie": "^1.6.0",
     "selenium-webdriver": "^2.53.1",
     "sinon": "^1.17.2",
diff --git a/translations/pt-BR.js b/translations/pt-BR.js
index 2074a079..8bff406f 100644
--- a/translations/pt-BR.js
+++ b/translations/pt-BR.js
@@ -25,6 +25,7 @@ module.exports = {
     dontSeeInField: 'naoVejoNoCampo',
     appendField: 'adicionaCampo',
     checkOption: 'marcoOpcao',
+    uncheckOption: 'desmarcoOpcao',
     seeCheckboxIsChecked: 'vejoCheckboxMarcado',
     dontSeeCheckboxIsChecked: 'naoVejoCheckboxMarcado',
     grabTextFrom: 'pegoTextoDe',


Test patch:
diff --git a/.circleci/test.sh b/.circleci/test.sh
index 6ad78178..8ff94f86 100755
--- a/.circleci/test.sh
+++ b/.circleci/test.sh
@@ -9,3 +9,4 @@ docker-compose run --rm test-helpers test/helper
 docker-compose run --rm test-helpers test/rest
 docker-compose run --rm test-acceptance.webdriverio
 docker-compose run --rm test-acceptance.nightmare
+docker-compose run --rm test-acceptance.puppeteer
diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js
new file mode 100644
index 00000000..8e959464
--- /dev/null
+++ b/test/acceptance/codecept.Puppeteer.js
@@ -0,0 +1,23 @@
+const TestHelper = require('../support/TestHelper.js');
+
+module.exports.config = {
+  tests: './*_test.js',
+  timeout: 10000,
+  output: './output',
+  helpers: {
+    Puppeteer: {
+      url: TestHelper.siteUrl(),
+      show: false,
+      chrome: {
+        args: [
+          '--no-sandbox',
+          '--disable-setuid-sandbox',
+        ],
+      },
+    },
+  },
+  include: {},
+  bootstrap: false,
+  mocha: {},
+  name: 'acceptance',
+};
diff --git a/test/acceptance/within_test.js b/test/acceptance/within_test.js
index 6878da7d..59e73b8e 100644
--- a/test/acceptance/within_test.js
+++ b/test/acceptance/within_test.js
@@ -1,6 +1,6 @@
 Feature('within');
 
-Scenario('within on form @WebDriverIO @Protractor @Nightmare', (I) => {
+Scenario('within on form @WebDriverIO @Protractor @Nightmare @Puppeteer', (I) => {
   I.amOnPage('/form/bug1467');
   I.see('TEST TEST');
   within({ css: '[name=form2]' }, () => {
@@ -11,7 +11,20 @@ Scenario('within on form @WebDriverIO @Protractor @Nightmare', (I) => {
   I.dontSeeCheckboxIsChecked({ css: 'form[name=form1] input[name=first_test_radio]' });
 });
 
-Scenario('within on iframe @WebDriverIO', (I) => {
+Scenario('switch iframe manually @WebDriverIO @Puppeteer', (I) => {
+  I.amOnPage('/iframe');
+
+  I.switchTo('iframe');
+  I.fillField('rus', 'Updated');
+  I.click('Sign in!');
+  I.waitForText('Email Address');
+
+  I.switchTo();
+  I.see('Iframe test');
+  I.dontSee('Email Address');
+});
+
+Scenario('within on iframe @WebDriverIO @Puppeteer', (I) => {
   I.amOnPage('/iframe');
   within({ frame: 'iframe' }, () => {
     I.fillField('rus', 'Updated');
@@ -22,7 +35,7 @@ Scenario('within on iframe @WebDriverIO', (I) => {
   I.dontSee('Email Address');
 });
 
-Scenario('within on iframe (without iframe navigation) @WebDriverIO @nightmare', (I) => {
+Scenario('within on iframe without iframe navigation @WebDriverIO @nightmare @Puppeteer', (I) => {
   I.amOnPage('/iframe');
   within({ frame: 'iframe' }, () => {
     I.fillField('rus', 'Updated');
@@ -32,7 +45,7 @@ Scenario('within on iframe (without iframe navigation) @WebDriverIO @nightmare',
   I.dontSee('Sign in!');
 });
 
-Scenario('within on nested iframe (without iframe navigation) (depth=2) @WebDriverIO @nightmare', (I) => {
+Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO @nightmare @Puppeteer', (I) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -42,7 +55,7 @@ Scenario('within on nested iframe (without iframe navigation) (depth=2) @WebDriv
   I.dontSee('Sign in!');
 });
 
-Scenario('within on nested iframe (depth=1) @WebDriverIO', (I) => {
+Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer', (I) => {
   I.amOnPage('/iframe');
   within({ frame: ['[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -53,7 +66,7 @@ Scenario('within on nested iframe (depth=1) @WebDriverIO', (I) => {
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe (depth=2) @WebDriverIO', (I) => {
+Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer', (I) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -64,7 +77,7 @@ Scenario('within on nested iframe (depth=2) @WebDriverIO', (I) => {
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe (depth=2) and mixed id and xpath selector @WebDriverIO', (I) => {
+Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDriverIO @Puppeteer', (I) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['#wrapperId', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
@@ -75,7 +88,7 @@ Scenario('within on nested iframe (depth=2) and mixed id and xpath selector @Web
   I.dontSee('Email Address');
 });
 
-Scenario('within on nested iframe (depth=2) and mixed class and xpath selector @WebDriverIO', (I) => {
+Scenario('within on nested iframe depth 2 and mixed class and xpath selector @WebDriverIO @Puppeteer', (I) => {
   I.amOnPage('/iframe_nested');
   within({ frame: ['.wrapperClass', '[name=content]'] }, () => {
     I.fillField('rus', 'Updated');
diff --git a/test/data/app/view/spinner.php b/test/data/app/view/spinner.php
index 9c1cf3c3..d1617cea 100644
--- a/test/data/app/view/spinner.php
+++ b/test/data/app/view/spinner.php
@@ -24,7 +24,7 @@
   setTimeout(function () {
     var spinner = document.querySelector('div.loader');
     spinner.parentNode.removeChild(spinner);
-  }, 500);
+  }, 750);
 </script>
 </body>
 </html>
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index ccb34330..74461124 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -2,7 +2,7 @@ version: '3'
 services:
   test-unit:
     build: ..
-    entrypoint: node_modules/.bin/gulp
+    entrypoint: docker/entrypoint node_modules/.bin/mocha
     command: test
     working_dir: /codecept
     volumes:
@@ -49,6 +49,18 @@ services:
       - ./support:/support
       - node_modules:/codecept/node_modules
 
+  test-acceptance.puppeteer:
+    build: ..
+    env_file: .env
+    environment:
+      - CODECEPT_ARGS=-c codecept.Puppeteer.js --grep @Puppeteer
+    depends_on:
+      - php
+    volumes:
+      - ./acceptance:/tests
+      - ./support:/support
+      - node_modules:/codecept/node_modules
+
   selenium.chrome:
     image: selenium/standalone-chrome:3.5.2-antimony
     ports:
diff --git a/test/helper/Nightmare_test.js b/test/helper/Nightmare_test.js
index 921f4527..9b850877 100644
--- a/test/helper/Nightmare_test.js
+++ b/test/helper/Nightmare_test.js
@@ -28,6 +28,7 @@ describe('Nightmare', function () {
       url: siteUrl,
       windowSize: '500x700',
       show: false,
+      waitForTimeout: 5000,
     });
     I._init();
     return I._beforeSuite();
@@ -101,7 +102,9 @@ describe('Nightmare', function () {
       }));
 
     it('should fail when test is not in context', () => I.amOnPage('/')
-      .then(() => I.see('debug', { css: 'a' }))
+      .then(() => I.see('debug', {
+        css: 'a',
+      }))
       .catch((e) => {
         e.should.be.instanceOf(AssertionFailedError);
         e.toString().should.not.include('web page');
@@ -113,7 +116,9 @@ describe('Nightmare', function () {
     it('should use locate to check element', () => {
       const attribute = 'qa-id';
       return I.amOnPage('/')
-        .then(() => I._locate({ css: '.notice' }).then((els) => {
+        .then(() => I._locate({
+          css: '.notice',
+        }).then((els) => {
           // we received an array with IDs of matched elements
           // now let's execute client-side script to get attribute for the first element
           assert.ok(!!els);
diff --git a/test/helper/ProtractorWeb_test.js b/test/helper/ProtractorWeb_test.js
index b321e228..c5116cb9 100644
--- a/test/helper/ProtractorWeb_test.js
+++ b/test/helper/ProtractorWeb_test.js
@@ -34,6 +34,7 @@ describe('Protractor-NonAngular', function () {
       angular: false,
       restart: false,
       seleniumAddress: TestHelper.seleniumAddress(),
+      waitForTimeout: 5000,
     });
     return I._init().then(() => I._beforeSuite().then(() => {
       browser = I.browser;
@@ -42,7 +43,10 @@ describe('Protractor-NonAngular', function () {
 
 
   beforeEach(() => {
-    webApiTests.init({ I, siteUrl });
+    webApiTests.init({
+      I,
+      siteUrl,
+    });
     return I._before();
   });
 
@@ -113,7 +117,9 @@ describe('Protractor-NonAngular', function () {
       }));
 
     it('should fail when test is not in context', () => I.amOnPage('/')
-      .then(() => I.see('debug', { css: 'a' }))
+      .then(() => I.see('debug', {
+        css: 'a',
+      }))
       .catch((e) => {
         e.should.be.instanceOf(AssertionFailedError);
         e.toString().should.not.include('web page');
diff --git a/test/helper/Protractor_test.js b/test/helper/Protractor_test.js
index 7b22368a..eb72f0db 100644
--- a/test/helper/Protractor_test.js
+++ b/test/helper/Protractor_test.js
@@ -33,6 +33,7 @@ describe('Protractor', function () {
       browser: 'chrome',
       seleniumAddress: TestHelper.seleniumAddress(),
       angular: true,
+      waitForTimeout: 5000,
     });
     return I._init().then(() => I._beforeSuite());
   });
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 6dd59dd4..94100296 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -1,4 +1,3 @@
-
 const TestHelper = require('../support/TestHelper');
 const Puppeteer = require('../../lib/helper/Puppeteer');
 const should = require('chai').should();
@@ -26,13 +25,21 @@ describe('Puppeteer', function () {
       url: siteUrl,
       windowSize: '500x700',
       show: false,
+      waitForTimeout: 2000,
+      waitForAction: 500,
+      chrome: {
+        args: ['--no-sandbox', '--disable-setuid-sandbox'],
+      },
+      defaultPopupAction: 'accept',
     });
     I._init();
     return I._beforeSuite();
   });
 
   beforeEach(() => {
-    webApiTests.init({ I, siteUrl });
+    webApiTests.init({
+      I, siteUrl,
+    });
     return I._before().then(() => {
       page = I.page;
       browser = I.browser;
@@ -118,4 +125,65 @@ describe('Puppeteer', function () {
       .then(() => I.switchToPreviousTab())
       .then(() => I.seeInCurrentUrl('/info')));
   });
+
+  describe('popup : #acceptPopup, #seeInPopup, #cancelPopup, #grabPopupText', () => {
+    it('should accept popup window', () => I.amOnPage('/form/popup')
+      .then(() => I.amAcceptingPopups())
+      .then(() => I.click('Confirm'))
+      .then(() => I.acceptPopup())
+      .then(() => I.see('Yes', '#result')));
+
+    it('should accept popup window (using default popup action type)', () => I.amOnPage('/form/popup')
+      .then(() => I.click('Confirm'))
+      .then(() => I.acceptPopup())
+      .then(() => I.see('Yes', '#result')));
+
+    it('should cancel popup', () => I.amOnPage('/form/popup')
+      .then(() => I.amCancellingPopups())
+      .then(() => I.click('Confirm'))
+      .then(() => I.cancelPopup())
+      .then(() => I.see('No', '#result')));
+
+    it('should check text in popup', () => I.amOnPage('/form/popup')
+      .then(() => I.amCancellingPopups())
+      .then(() => I.click('Alert'))
+      .then(() => I.seeInPopup('Really?'))
+      .then(() => I.cancelPopup()));
+
+    it('should grab text from popup', () => I.amOnPage('/form/popup')
+      .then(() => I.amCancellingPopups())
+      .then(() => I.click('Alert'))
+      .then(() => I.grabPopupText())
+      .then(text => assert.equal(text, 'Really?')));
+
+    it('should return null if no popup is visible (do not throw an error)', () => I.amOnPage('/form/popup')
+      .then(() => I.grabPopupText())
+      .then(text => assert.equal(text, null)));
+  });
+
+  describe('#switchTo', () => {
+    it('should switch reference to iframe content', () => I.amOnPage('/iframe')
+      .then(() => I.switchTo('[name="content"]'))
+      .then(() => I.see('Information\nLots of valuable data here')));
+
+    it('should return error if iframe selector is invalid', () => I.amOnPage('/iframe')
+      .then(() => I.switchTo('#invalidIframeSelector'))
+      .catch((e) => {
+        e.should.be.instanceOf(Error);
+        e.message.should.be.equal('Element #invalidIframeSelector was not found by text|CSS|XPath');
+      }));
+
+    it('should return error if iframe selector is not iframe', () => I.amOnPage('/iframe')
+      .then(() => I.switchTo('h1'))
+      .catch((e) => {
+        e.should.be.instanceOf(Error);
+        e.message.should.be.equal('Element #invalidIframeSelector was not found by text|CSS|XPath');
+      }));
+
+    it('should return to parent frame given a null locator', () => I.amOnPage('/iframe')
+      .then(() => I.switchTo('[name="content"]'))
+      .then(() => I.see('Information\nLots of valuable data here'))
+      .then(() => I.switchTo(null))
+      .then(() => I.see('Iframe test')));
+  });
 });
diff --git a/test/helper/WebDriverIO_test.js b/test/helper/WebDriverIO_test.js
index b8d2179f..bb1b2576 100644
--- a/test/helper/WebDriverIO_test.js
+++ b/test/helper/WebDriverIO_test.js
@@ -30,6 +30,7 @@ describe('WebDriverIO', function () {
       smartWait: 0, // just to try
       host: TestHelper.seleniumHost(),
       port: TestHelper.seleniumPort(),
+      waitForTimeout: 5000,
     });
   });
 
@@ -147,17 +148,32 @@ describe('WebDriverIO', function () {
 
   describe('#seeAttributesOnElements', () => {
     it('should check attributes values for given element', () => wd.amOnPage('/info')
-      .then(() => wd.seeAttributesOnElements('//form', { method: 'post' }))
-      .then(() => wd.seeAttributesOnElements('//form', { method: 'post', action: `${siteUrl}/` }))
-      .then(() => wd.seeAttributesOnElements('//form', { method: 'get' }))
+      .then(() => wd.seeAttributesOnElements('//form', {
+        method: 'post',
+      }))
+      .then(() => wd.seeAttributesOnElements('//form', {
+        method: 'post',
+        action: `${siteUrl}/`,
+      }))
+      .then(() => wd.seeAttributesOnElements('//form', {
+        method: 'get',
+      }))
       .catch((e) => {
         assert.equal(e.message, 'Not all elements (//form) have attributes {"method":"get"}');
       }));
 
     it('should check attributes values for several elements', () => wd.amOnPage('/')
-      .then(() => wd.seeAttributesOnElements('a', { 'qa-id': 'test', 'qa-link': 'test' }))
-      .then(() => wd.seeAttributesOnElements('//div', { 'qa-id': 'test' }))
-      .then(() => wd.seeAttributesOnElements('a', { 'qa-id': 'test', href: '/info' }))
+      .then(() => wd.seeAttributesOnElements('a', {
+        'qa-id': 'test',
+        'qa-link': 'test',
+      }))
+      .then(() => wd.seeAttributesOnElements('//div', {
+        'qa-id': 'test',
+      }))
+      .then(() => wd.seeAttributesOnElements('a', {
+        'qa-id': 'test',
+        href: '/info',
+      }))
       .catch((e) => {
         e.message.should.include('Not all elements (a) have attributes {"qa-id":"test","href":"/info"}');
       }));
@@ -190,17 +206,32 @@ describe('WebDriverIO', function () {
 
   describe('#seeCssPropertiesOnElements', () => {
     it('should check css property for given element', () => wd.amOnPage('/info')
-      .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': 'bold' }))
-      .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': 'bold', display: 'block' }))
-      .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': 'non-bold' }))
+      .then(() => wd.seeCssPropertiesOnElements('h3', {
+        'font-weight': 'bold',
+      }))
+      .then(() => wd.seeCssPropertiesOnElements('h3', {
+        'font-weight': 'bold',
+        display: 'block',
+      }))
+      .then(() => wd.seeCssPropertiesOnElements('h3', {
+        'font-weight': 'non-bold',
+      }))
       .catch((e) => {
         e.message.should.include('Not all elements (h3) have CSS property {"font-weight":"non-bold"}');
       }));
 
     it('should check css property for several elements', () => wd.amOnPage('/')
-      .then(() => wd.seeCssPropertiesOnElements('a', { color: 'rgba(0, 0, 238, 1)', cursor: 'auto' }))
-      .then(() => wd.seeCssPropertiesOnElements('//div', { display: 'block' }))
-      .then(() => wd.seeCssPropertiesOnElements('a', { 'margin-top': '0em', cursor: 'auto' }))
+      .then(() => wd.seeCssPropertiesOnElements('a', {
+        color: 'rgba(0, 0, 238, 1)',
+        cursor: 'auto',
+      }))
+      .then(() => wd.seeCssPropertiesOnElements('//div', {
+        display: 'block',
+      }))
+      .then(() => wd.seeCssPropertiesOnElements('a', {
+        'margin-top': '0em',
+        cursor: 'auto',
+      }))
       .catch((e) => {
         e.message.should.include('Not all elements (a) have CSS property {"margin-top":"0em","cursor":"auto"}');
       }));
@@ -217,7 +248,9 @@ describe('WebDriverIO', function () {
       .then(() => wd.grabNumberOfVisibleElements('//div[@id = "grab-multiple"]//a'))
       .then(num => assert.equal(num, 3)));
     it('should support locators like {xpath:"//div"}', () => wd.amOnPage('/info')
-      .then(() => wd.grabNumberOfVisibleElements({ xpath: '//div[@id = "grab-multiple"]//a' }))
+      .then(() => wd.grabNumberOfVisibleElements({
+        xpath: '//div[@id = "grab-multiple"]//a',
+      }))
       .then(num => assert.equal(num, 3)));
   });
 
@@ -244,7 +277,9 @@ describe('WebDriverIO', function () {
 
     it('should create a screenshot on fail  @ups', () => {
       const sec = (new Date()).getUTCMilliseconds().toString();
-      const test = { title: `sw should do smth ${sec}` };
+      const test = {
+        title: `sw should do smth ${sec}`,
+      };
       return wd.amOnPage('/')
         .then(() => wd._failed(test))
         .then(() => assert.ok(fileExists(path.join(output_dir, `sw_should_do_smth_${sec}.failed.png`)), null, 'file does not exists'));
@@ -319,6 +354,15 @@ describe('WebDriverIO', function () {
       .then(() => wd.click('Alert'))
       .then(() => wd.seeInPopup('Really?'))
       .then(() => wd.cancelPopup()));
+
+    it('should grab text from popup', () => wd.amOnPage('/form/popup')
+      .then(() => wd.click('Alert'))
+      .then(() => wd.grabPopupText())
+      .then(text => assert.equal(text, 'Really?')));
+
+    it('should return null if no popup is visible (do not throw an error)', () => wd.amOnPage('/form/popup')
+      .then(() => wd.grabPopupText())
+      .then(text => assert.equal(text, null)));
   });
 
   describe('#waitForText', () => {
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index 36eb43dc..c59d77b8 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -55,7 +55,9 @@ module.exports.tests = function () {
       yield I.amOnPage('/');
       yield I.see('Welcome to test app!', 'h1');
       yield I.amOnPage('/info');
-      yield I.see('valuable', { css: 'p' });
+      yield I.see('valuable', {
+        css: 'p',
+      });
       yield I.see('valuable', '//body/p');
       return I.dontSee('valuable', 'h1');
     });
@@ -287,7 +289,9 @@ module.exports.tests = function () {
 
     it('should fill field by strict locator', function* () {
       yield I.amOnPage('/form/field');
-      yield I.fillField({ id: 'name' }, 'Nothing special');
+      yield I.fillField({
+        id: 'name',
+      }, 'Nothing special');
       yield I.click('Submit');
       return assert.equal(formContents('name'), 'Nothing special');
     });
@@ -415,13 +419,17 @@ module.exports.tests = function () {
 
     it('should grab attribute from element', function* () {
       yield I.amOnPage('/search');
-      const val = yield I.grabAttributeFrom({ css: 'form' }, 'method');
+      const val = yield I.grabAttributeFrom({
+        css: 'form',
+      }, 'method');
       return assert.equal(val, 'get');
     });
 
     it('should grab custom attribute from element', function* () {
       yield I.amOnPage('/form/example4');
-      const val = yield I.grabAttributeFrom({ css: '.navbar-toggle' }, 'data-toggle');
+      const val = yield I.grabAttributeFrom({
+        css: '.navbar-toggle',
+      }, 'data-toggle');
       return assert.equal(val, 'collapse');
     });
   });
@@ -485,7 +493,9 @@ module.exports.tests = function () {
 
     it('should create a screenshot on fail  @ups', () => {
       const sec = (new Date()).getUTCMilliseconds().toString();
-      const test = { title: `sw should do smth ${sec}` };
+      const test = {
+        title: `sw should do smth ${sec}`,
+      };
       return I.amOnPage('/')
         .then(() => I._failed(test))
         .then(() => assert.ok(fileExists(path.join(global.output_dir, `sw_should_do_smth_${sec}.failed.png`)), null, 'file does not exists'));
@@ -494,7 +504,10 @@ module.exports.tests = function () {
 
   describe('cookies : #setCookie, #clearCookies, #seeCookie', () => {
     it('should do all cookie stuff', () => I.amOnPage('/')
-      .then(() => I.setCookie({ name: 'auth', value: '123456' }))
+      .then(() => I.setCookie({
+        name: 'auth',
+        value: '123456',
+      }))
       .then(() => I.seeCookie('auth'))
       .then(() => I.dontSeeCookie('auuth'))
       .then(() => I.grabCookie('auth'))
@@ -503,7 +516,10 @@ module.exports.tests = function () {
       .then(() => I.dontSeeCookie('auth')));
 
     it('should clear all cookies', () => I.amOnPage('/')
-      .then(() => I.setCookie({ name: 'auth', value: '123456' }))
+      .then(() => I.setCookie({
+        name: 'auth',
+        value: '123456',
+      }))
       .then(() => I.clearCookie())
       .then(() => I.dontSeeCookie('auth')));
   });
@@ -581,11 +597,10 @@ module.exports.tests = function () {
 
   describe('#waitUntilExists', () => {
     it('should wait for an element to be removed from DOM', () => I.amOnPage('/spinner')
-      .then(() => I.seeElementInDOM('.loader'))
       .then(() => I.waitUntilExists('.loader'))
       .then(() => I.dontSeeElement('.loader')));
 
-    it('should wait for a non-exising element to be removed from DOM', () => I.amOnPage('/spinner')
+    it('should wait for a non-existing element to be removed from DOM', () => I.amOnPage('/spinner')
       .then(() => I.dontSeeElement('.non-existing-class'))
       .then(() => I.waitUntilExists('.non-existing-class'))
       .then(() => I.dontSeeElement('.non-existing-class')));
@@ -612,7 +627,9 @@ module.exports.tests = function () {
         .then(() => I.seeInField('Hasło', '12345'))
         .then(() => I.checkOption('terms'))
         .then(() => I.seeCheckboxIsChecked('terms'))
-        .then(() => I._withinBegin({ css: '.form-group' }))
+        .then(() => I._withinBegin({
+          css: '.form-group',
+        }))
         .then(() => I.see('E-Mail'))
         .then(() => I.dontSee('Hasło'))
         .then(() => I.dontSeeElement('#navbar-collapse-menu'))
@@ -632,7 +649,9 @@ module.exports.tests = function () {
 
     it('should execute within block 2', () => I.amOnPage('/form/example4')
       .then(() => I.fillField('Hasło', '12345'))
-      .then(() => I._withinBegin({ xpath: '//div[@class="form-group"][2]' }))
+      .then(() => I._withinBegin({
+        xpath: '//div[@class="form-group"][2]',
+      }))
       .then(() => I.dontSee('E-Mail'))
       .then(() => I.see('Hasło'))
       .then(() => I.grabTextFrom('label'))
@@ -642,7 +661,9 @@ module.exports.tests = function () {
 
     it('within should respect context in see', () => I.amOnPage('/form/example4')
       .then(() => I.see('Rejestracja', 'fieldset'))
-      .then(() => I._withinBegin({ css: '.navbar-header' }))
+      .then(() => I._withinBegin({
+        css: '.navbar-header',
+      }))
       .then(() => I.see('Rejestracja', '.container fieldset'))
       .catch((err) => {
         if (!err) assert.fail('seen fieldset');
@@ -653,7 +674,9 @@ module.exports.tests = function () {
       }));
 
     it('within should respect context in see when using nested frames', () => I.amOnPage('/iframe_nested')
-      .then(() => I._withinBegin({ frame: ['#wrapperId', '[name=content]'] }))
+      .then(() => I._withinBegin({
+        frame: ['#wrapperId', '[name=content]'],
+      }))
       .then(() => I.see('Kill & Destroy'))
       .catch((err) => {
         if (!err) assert.fail('seen "Kill & Destroy"');
diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js
index cf480ed1..4e5f3482 100644
--- a/test/rest/REST_test.js
+++ b/test/rest/REST_test.js
@@ -38,7 +38,7 @@ describe('REST', () => {
     } catch (err) {
       // continue regardless of error
     }
-    setTimeout(done, 700);
+    setTimeout(done, 1000);
   });
 
   describe('basic requests', () => {
@@ -46,14 +46,22 @@ describe('REST', () => {
       response.body.name.should.eql('davert');
     }));
 
+    it('should send PATCH requests', () => I.sendPatchRequest('/user', { email: 'user@user.com' }).then((response) => {
+      const body = JSON.parse(Object.keys(response.body)[1]);
+      body.email.should.eql('user@user.com');
+    }));
+
     it('should send POST requests', () => I.sendPostRequest('/user', { name: 'john' }).then((response) => {
-      response.body.name.should.eql('john');
+      const body = JSON.parse(Object.keys(response.body)[0]);
+      body.name.should.eql('john');
     }));
 
     it('should send PUT requests', () => I.sendPutRequest('/posts/1', { author: 'john' }).then((response) => {
-      response.body.author.should.eql('john');
+      const body = JSON.parse(Object.keys(response.body)[0]);
+      body.author.should.eql('john');
       return I.sendGetRequest('/posts/1').then((response) => {
-        response.body.author.should.eql('john');
+        const body = JSON.parse(Object.keys(response.body)[0]);
+        body.author.should.eql('john');
       });
     }));
 
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 99483946..b9dd9e47 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -55,11 +55,15 @@ describe('CodeceptJS Interface', () => {
         '[1] Starting recording promises',
         '[1] Queued | hook FileSystem._beforeSuite()',
         '[1] Queued | hook FileSystem._before()',
+        '[1] Queued | hook FileSystem._beforeStep()',
         '[1] Queued | amInPath: "."',
+        '[1] Queued | hook FileSystem._afterStep()',
         '[1] Queued | step passed',
         '[1] Queued | return result',
         '[1] Queued | say hello world',
+        '[1] Queued | hook FileSystem._beforeStep()',
         '[1] Queued | seeFile: "codecept.json"',
+        '[1] Queued | hook FileSystem._afterStep()',
         '[1] Queued | step passed',
         '[1] Queued | return result',
         '[1] Queued | fire test.passed',
diff --git a/test/unit/steps_test.js b/test/unit/steps_test.js
index c5a460c0..abadbe86 100644
--- a/test/unit/steps_test.js
+++ b/test/unit/steps_test.js
@@ -25,7 +25,7 @@ describe('Step', () => {
     step.humanizeArgs().should.eql('"word", 1');
 
     step.args = [['some', 'data'], 1];
-    step.humanizeArgs().should.eql('[some,data], 1');
+    step.humanizeArgs().should.eql('["some","data"], 1');
 
     step.args = [{ css: '.class' }];
     step.humanizeArgs().should.eql('{"css":".class"}');