Repository: Codeception/CodeceptJS
Pull request #584 (2017-06-30 15:36:52)

[WIP] Within generators
Our goals:
- [x] Support generators in within blocks
- [x] Refactor predefined methods to work in within properly:
  - [x] WebdriverIO
  - [x] SeleniumWebdriver/Protractor
  - [x] Nightmare
- [x] Add tests for within

Bonus:
- [x] Fix https://github.com/Codeception/CodeceptJS/issues/529

DavertMik: Could you remove all unnecessary files from this config? 
Like changes in linting and formatting codestyle.

Patch:
diff --git a/docs/data.md b/docs/data.md
index 865e4c5b..4bdd769e 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -15,7 +15,7 @@ API supposed to be a stable interface and it can be used by acceptance tests. Co
 
 ## REST
 
-REST helper uses [Unirest](http://unirest.io/nodejs.html) library to send HTTP requests to application.
+[REST helper](http://codecept.io/helpers/REST/) uses [Unirest](http://unirest.io/nodejs.html) library to send HTTP requests to application.
 However, it doesn't provide tools for testing APIs, so it should be paired with WebDriverIO, Nightmare or Protractor helpers for browser testing.
 
 Enable REST helper in global config. It is recommended to set `endpoint`, a base URL for all API requests.
@@ -71,7 +71,7 @@ I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest
 
 ## Data Generation with Factories
 
-This concept is extended by `ApiDataFactory` helper.
+This concept is extended by [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory/) helper.
 It builds data according to defined rules and uses API to store them and automatically clean them up after a test,
 This way setting data for a test is as simple as writing:
 
@@ -80,7 +80,7 @@ let post = yield I.have('post');
 I.haveMultiple('comment', 5, { postId: post.id});
 ```
 Just define how many items of any kind you need and ApiDataFactory will create them for you.
-However, to make this work some preperations required.
+However, to make this work some preparations required.
 
 At first, you need data generation libraries which are [Rosie](https://github.com/rosiejs/rosie) and [Faker](https://www.npmjs.com/package/faker). Faker can generate random names, emails, texts, and Rosie uses them
 to generate objects using factories.
diff --git a/docs/helpers/WebDriverIO.md b/docs/helpers/WebDriverIO.md
index 119058a5..5c9dbf5d 100644
--- a/docs/helpers/WebDriverIO.md
+++ b/docs/helpers/WebDriverIO.md
@@ -612,6 +612,7 @@ assert(cookie.value, '123456');
 
 Retrieves the innerHTML from an element located by CSS or XPath and returns it to test.
 Resumes test execution, so **should be used inside a generator with `yield`** operator.
+In `within` block will search element on page, not in `within` element
 Appium: support only web testing
 
 ```js
diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js
index d0e12233..ce0c2de3 100644
--- a/lib/helper/ApiDataFactory.js
+++ b/lib/helper/ApiDataFactory.js
@@ -96,7 +96,7 @@ let unirest = require('unirest');
  * ```
 
  * It is required to set REST API `endpoint` which is the baseUrl for all API requests.
- * Factories are listed, factory file is expected to be passed via `factory` option.
+ * Factory file is expected to be passed via `factory` option.
  *
  * This Helper uses [REST](http://codecept.io/helpers/REST/) helper and accepts its configuration in "REST" section.
  * So, in order to set default headers or timeout you should add:
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 36bb5fee..4885f5c1 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -197,6 +197,9 @@ class Appium extends WebdriverIO {
   }
 
   _withinBegin(context) {
+    if (this.isWeb) {
+      return super._withinBegin(context);
+    }
     if (context == 'webview') {
       return this.switchToWeb();
     }
@@ -208,6 +211,9 @@ class Appium extends WebdriverIO {
   }
 
   _withinEnd() {
+    if (this.isWeb) {
+      return super._withinEnd();
+    }
     return this.switchToNative();
   }
 
@@ -1052,6 +1058,7 @@ class Appium extends WebdriverIO {
    *
    */
   appendField(field, value) {
+    if (this.isWeb) return super.appendField(field, value);
     return super.appendField(parseLocator.call(this, field), value);
   }
 
@@ -1060,6 +1067,7 @@ class Appium extends WebdriverIO {
    *
    */
   checkOption(checkbox) {
+    if (this.isWeb) return super.checkOption(checkbox);
     return super.checkOption(parseLocator.call(this, checkbox));
   }
 
@@ -1068,6 +1076,7 @@ class Appium extends WebdriverIO {
    *
    */
   click(button, context) {
+    if (this.isWeb) return super.click(button, context);
     return super.click(parseLocator.call(this, button), parseLocator.call(this, context));
   }
 
@@ -1076,6 +1085,7 @@ class Appium extends WebdriverIO {
    *
    */
   dontSeeCheckboxIsChecked(checkbox) {
+    if (this.isWeb) return super.dontSeeCheckboxIsChecked(checkbox);
     return super.dontSeeCheckboxIsChecked(parseLocator.call(this, checkbox));
   }
 
@@ -1084,6 +1094,7 @@ class Appium extends WebdriverIO {
    *
    */
   dontSeeElement(el, context) {
+    if (this.isWeb) return super.dontSeeElement(el, context);
     return super.dontSeeElement(parseLocator.call(this, el));
   }
 
@@ -1092,6 +1103,7 @@ class Appium extends WebdriverIO {
    *
    */
   dontSeeInField(field, value) {
+    if (this.isWeb) return super.dontSeeInField(field, value);
     return super.dontSeeInField(parseLocator.call(this, field), value);
   }
 
@@ -1101,6 +1113,7 @@ class Appium extends WebdriverIO {
    *
    */
   dontSee(text, context) {
+    if (this.isWeb) return super.dontSee(text, context);
     return super.dontSee(text, parseLocator.call(this, context));
   }
 
@@ -1109,6 +1122,7 @@ class Appium extends WebdriverIO {
    *
    */
   fillField(field, value) {
+    if (this.isWeb) return super.fillField(field, value);
     return super.fillField(parseLocator.call(this, field), value);
   }
 
@@ -1117,6 +1131,7 @@ class Appium extends WebdriverIO {
    *
    */
   grabTextFrom(el) {
+    if (this.isWeb) return super.grabTextFrom(el);
     return super.grabTextFrom(parseLocator.call(this, el));
   }
 
@@ -1125,6 +1140,7 @@ class Appium extends WebdriverIO {
    *
    */
   grabValueFrom(el) {
+    if (this.isWeb) return super.grabValueFrom(el);
     return super.grabValueFrom(parseLocator.call(this, el));
   }
 
@@ -1133,6 +1149,7 @@ class Appium extends WebdriverIO {
    *
    */
   seeCheckboxIsChecked(checkbox) {
+    if (this.isWeb) return super.seeCheckboxIsChecked(checkbox);
     return super.seeCheckboxIsChecked(parseLocator.call(this, checkbox));
   }
 
@@ -1141,6 +1158,7 @@ class Appium extends WebdriverIO {
    *
    */
   seeElement(el) {
+    if (this.isWeb) return super.seeElement(el);
     return super.seeElement(parseLocator.call(this, el));
   }
 
@@ -1148,7 +1166,8 @@ class Appium extends WebdriverIO {
    * {{> ../webapi/seeInField }}
    *
    */
-  seeInField() {
+  seeInField(field, value) {
+    if (this.isWeb) return super.seeInField(field, value);
     return super.seeInField(parseLocator.call(this, field), value);
   }
 
@@ -1157,6 +1176,7 @@ class Appium extends WebdriverIO {
    *
    */
   see(text, context) {
+    if (this.isWeb) return super.see(text, context);
     return super.see(text, parseLocator.call(this, context));
   }
 
@@ -1167,8 +1187,8 @@ class Appium extends WebdriverIO {
    *
    */
   selectOption(select, option) {
-    if (!this.isWeb) throw new Error(`Should be used only in Web context. In native context use 'click' method instead`);
-    return super.selectOption(parseLocator.call(this, select), option);
+    if (this.isWeb) return super.selectOption(select, option);
+    throw new Error(`Should be used only in Web context. In native context use 'click' method instead`);
   }
 
 
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index 56f7f0f7..2620aa7f 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -17,6 +17,8 @@ let specialKeys = {
   Delete: '\u007f'
 };
 
+let withinStatus = false;
+
 /**
  * Nightmare helper wraps [Nightmare](https://github.com/segmentio/nightmare) library to provide
  * fastest headless testing using Electron engine. Unlike Selenium-based drivers this uses
@@ -228,7 +230,8 @@ class Nightmare extends Helper {
   _withinBegin(locator) {
     this.context = locator;
     locator = guessLocator(locator) || {css: locator};
-    this.browser.evaluate(function (by, locator) {
+    withinStatus = true;
+    return this.browser.evaluate(function (by, locator) {
       var el = codeceptjs.findElement(by, locator);
       if (!el) throw new Error(`Element by ${by}: ${locator} not found`);
       window.codeceptjs.within = el;
@@ -237,6 +240,7 @@ class Nightmare extends Helper {
 
   _withinEnd() {
     this.context = this.options.rootElement;
+    withinStatus = false;
     return this.browser.evaluate(function () {
       codeceptjs.within = null;
     });
@@ -855,7 +859,10 @@ class Nightmare extends Helper {
 
   _failed(test) {
     let fileName = test.title.replace(/\W/g, '_') + '.failed.png';
-    return this.saveScreenshot(fileName, true);
+    let promisesList = [];
+    if (withinStatus !== false) promisesList.push(this._withinEnd());
+    promisesList.push(this.saveScreenshot(fileName, true));
+    return Promise.all(promisesList);
   }
 
   /**
@@ -884,11 +891,12 @@ module.exports = Nightmare;
 function proceedSee(assertType, text, context) {
   let description, locator;
   if (!context) {
-    locator = guessLocator(this.context) || {css: this.context};
     if (this.context === this.options.rootElement) {
+      locator = guessLocator(this.context) || {css: this.context};
       description = 'web application';
     } else {
       description = 'current context ' + this.context; [];
+      locator = {xpath: './/*'};
     }
   } else {
     locator = guessLocator(context) || {css: context};
diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js
index de153256..892f18a8 100644
--- a/lib/helper/Protractor.js
+++ b/lib/helper/Protractor.js
@@ -165,6 +165,7 @@ class Protractor extends SeleniumWebdriver {
   _withinEnd() {
     this.browser.findElement = withinStore.elFn;
     this.browser.findElements = withinStore.elsFn;
+    withinStore = {};
     this.context = this.options.rootElement;
   }
 
diff --git a/lib/helper/SeleniumWebdriver.js b/lib/helper/SeleniumWebdriver.js
index d52eb0fd..a739dafa 100644
--- a/lib/helper/SeleniumWebdriver.js
+++ b/lib/helper/SeleniumWebdriver.js
@@ -163,7 +163,11 @@ class SeleniumWebdriver extends Helper {
 
   _failed(test) {
     let fileName = test.title.replace(/ /g, '_') + '.failed.png';
-    return this.saveScreenshot(fileName, true);
+    let promisesList = [];
+    console.log('within', withinStore);
+    if (Object.keys(withinStore).length != 0) promisesList.push(this._withinEnd());
+    promisesList.push(this.saveScreenshot(fileName, true));
+    return Promise.all(promisesList);
   }
 
   _withinBegin(locator) {
@@ -171,16 +175,17 @@ class SeleniumWebdriver extends Helper {
     withinStore.elsFn = this.browser.findElements;
 
     this.context = locator;
-    let context = this.browser.findElement(guessLocator(locator) || by.css(locator));
-
-    this.browser.findElement = (l) => context.findElement(l);
-    this.browser.findElements = (l) => context.findElements(l);
-    return context;
+    return this.browser.findElement(guessLocator(locator) || by.css(locator)).then((context) => {
+      this.browser.findElement = (l) => context.findElement(l);
+      this.browser.findElements = (l) => context.findElements(l);
+      return context;
+    });
   }
 
   _withinEnd() {
     this.browser.findElement = withinStore.elFn;
     this.browser.findElements = withinStore.elsFn;
+    withinStore = {};
     this.context = this.options.rootElement;
   }
 
@@ -244,14 +249,14 @@ class SeleniumWebdriver extends Helper {
   /**
    * {{> ../webapi/see }}
    */
-  see(text, context) {
+  see(text, context = null) {
     return proceedSee.call(this, 'assert', text, context);
   }
 
   /**
    * {{> ../webapi/dontSee }}
    */
-  dontSee(text, context) {
+  dontSee(text, context = null) {
     return proceedSee.call(this, 'negate', text, context);
   }
 
@@ -814,7 +819,8 @@ function proceedSee(assertType, text, context) {
       locator = guessLocator(this.context) || by.css(this.context);
       description = 'web application';
     } else {
-      locator = null;
+      // inside within block
+      locator = by.xpath('.//*');
       description = 'current context ' + this.context;
     }
   } else {
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index 83cd05f1..62098053 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -3,7 +3,6 @@ let webdriverio;
 const Helper = require('../helper');
 const stringIncludes = require('../assert/include').includes;
 const urlEquals = require('../assert/equal').urlEquals;
-const equals = require('../assert/equal').equals;
 const empty = require('../assert/empty').empty;
 const truth = require('../assert/truth').truth;
 const xpathLocator = require('../utils').xpathLocator;
@@ -307,10 +306,14 @@ class WebDriverIO extends Helper {
 
   _after() {
     if (this.options.restart) return this.browser.end();
-    if (this.options.keepCookies) return Promise.all([this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
+    if (this.options.keepCookies) {
+      return Promise.all(
+        [this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
+    }
     if (this.options.desiredCapabilities.browserName) {
       this.debugSection('Session', 'cleaning cookies and localStorage');
-      return Promise.all([this.browser.deleteCookie(), this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
+      return Promise.all(
+        [this.browser.deleteCookie(), this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
     }
   }
 
@@ -325,24 +328,29 @@ class WebDriverIO extends Helper {
     let fileName = '';
     if (this.options.uniqueScreenshotNames) {
       fileName =
-        test.title.substring(0, 10).replace(/ /g, '_') + '-' + hashCode(test.title) + '-' + hashCode(test.file) +
+        test.title.substring(0, 10).replace(/ /g, '_') + '-' + hashCode(test.title) + '-' +
+        hashCode(test.file) +
         '.failed.png';
     } else {
       fileName = test.title.replace(/ /g, '_') + '.failed.png';
     }
-    return this.saveScreenshot(fileName, this.options.fullPageScreenshots);
+    let promisesList = [];
+    if (Object.keys(withinStore).length != 0) promisesList.push(this._withinEnd());
+    promisesList.push(this.saveScreenshot(fileName, this.options.fullPageScreenshots));
+    return Promise.all(promisesList);
   }
 
   _withinBegin(locator) {
     let frame = isFrameLocator(locator);
+    let client = this.browser;
     if (frame) {
       withinStore.frame = frame;
-      return this.browser.element(frame).then((res) => this.browser.frame(res.value));
+      return this.switchTo(frame);
     }
     withinStore.elFn = this.browser.element;
     withinStore.elsFn = this.browser.elements;
     this.context = locator;
-    return this.browser.element(withStrictLocator(locator)).then((res) => {
+    return client.element(withStrictLocator(locator)).then((res) => {
       this.browser.element = function (l) {
         return this.elementIdElement(res.value.ELEMENT, l);
       };
@@ -355,7 +363,7 @@ class WebDriverIO extends Helper {
   _withinEnd() {
     if (withinStore.frame) {
       withinStore = {};
-      return this.browser.frame(null);
+      return this.switchTo(null);
     }
     this.context = this.root;
     this.browser.element = withinStore.elFn;
@@ -491,7 +499,22 @@ class WebDriverIO extends Helper {
    * Appium: support, but in apps works as usual click
    */
   rightClick(locator) {
-    return this.browser.rightClick(withStrictLocator(locator));
+    let client = this.browser;
+    /**
+     * just press button if no selector is given
+     */
+    if (locator === undefined) {
+      return client.buttonPress("right");
+    }
+    return this._locate(locator).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        if (typeof locator === "object") locator = JSON.stringify(locator);
+        throw new Error(`Clickable element ${locator.toString()} was not found by text|CSS|XPath`);
+      }
+      let elem = res.value[0];
+      if (client.isMobile) return this.touchClick(elem.ELEMENT);
+      return this.moveTo(elem.ELEMENT).buttonPress("right");
+    });
   }
 
   /**
@@ -626,8 +649,18 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   grabTextFrom(locator) {
-    return this.browser.getText(withStrictLocator(locator)).then(function (text) {
-      return text;
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(client.elementIdText(el.ELEMENT)));
+      return client.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return selected;
+      });
     });
   }
 
@@ -636,6 +669,7 @@ class WebDriverIO extends Helper {
    * Resumes test execution, so **should be used inside a generator with `yield`** operator.
    * Appium: support only web testing
    *
+   *
    * ```js
    * let postHTML = yield I.grabHTMLFrom('#post');
    * ```
@@ -651,8 +685,18 @@ class WebDriverIO extends Helper {
    * Appium: support only web testing
    */
   grabValueFrom(locator) {
-    return this.browser.getValue(withStrictLocator(locator)).then(function (text) {
-      return text;
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(client.elementIdAttribute(el.ELEMENT, 'value')));
+      return client.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return selected;
+      });
     });
   }
 
@@ -661,8 +705,18 @@ class WebDriverIO extends Helper {
    * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
    */
   grabAttributeFrom(locator, attr) {
-    return this.browser.getAttribute(withStrictLocator(locator), attr).then(function (text) {
-      return text;
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(client.elementIdAttribute(el.ELEMENT, attr)));
+      return client.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return selected;
+      });
     });
   }
 
@@ -750,8 +804,17 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   seeElement(locator) {
-    return this.browser.isVisible(withStrictLocator(locator)).then(function (res) {
-      return truth(`elements of ${locator}`, 'to be seen').assert(res);
+    return this.browser.elements(withStrictLocator(locator)).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        return truth(`elements of ${locator}`, 'to be seen').assert(false);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+      return this.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return truth(`elements of ${locator}`, 'to be seen').assert(selected);
+      });
     });
   }
 
@@ -760,8 +823,17 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   dontSeeElement(locator) {
-    return this.browser.isVisible(withStrictLocator(locator)).then(function (res) {
-      return truth(`elements of ${locator}`, 'to be seen').negate(res);
+    return this.browser.elements(withStrictLocator(locator)).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        return truth(`elements of ${locator}`, 'to be seen').negate(false);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+      return this.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return truth(`elements of ${locator}`, 'to be seen').negate(selected);
+      });
     });
   }
 
@@ -825,7 +897,7 @@ class WebDriverIO extends Helper {
   seeNumberOfElements(selector, num) {
     return this.browser.elements(withStrictLocator(selector))
       .then(function (res) {
-        return assert.equal(res.value.length, num);
+        return assert.equal(res.value.length, num, `expected nummber of elements (${selector}) is ${num}, but found ${res.value.length}`);
       });
   }
 
@@ -838,12 +910,20 @@ class WebDriverIO extends Helper {
    * ```
    */
   seeNumberOfVisibleElements(locator, num) {
-    return this.browser.isVisible(withStrictLocator(locator))
-      .then(function (res) {
-        if (!Array.isArray(res)) res = [res];
-        res = res.filter((val) => val == true);
-        return assert.equal(res.length, num);
+    return this.browser.elements(locator).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        return truth(`elements of ${selector}`, 'to be seen').assert(false);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+      return this.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        if (!Array.isArray(selected)) selected = [selected];
+        selected = selected.filter((val) => val === true);
+        return assert.equal(selected.length, num, `expected nummber of visible elements (${locator}) is ${num}, but found ${selected.length}`);
       });
+    });
   }
 
   /**
@@ -915,7 +995,39 @@ class WebDriverIO extends Helper {
    * ```
    */
   scrollTo(locator, offsetX, offsetY) {
-    return this.browser.scroll(withStrictLocator(locator), offsetX, offsetY);
+    let client = this.browser;
+    offsetX = typeof offsetX === 'number' ? offsetX : 0;
+    offsetY = typeof offsetY === 'number' ? offsetY : 0;
+
+    if (typeof locator === 'number' && typeof offsetX === 'number') {
+      offsetY = offsetX;
+      offsetX = locator;
+      locator = null;
+    }
+
+    if (locator) {
+      return client.element(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return truth(`elements of ${locator}`, 'to be seen').assert(false);
+        }
+        let elem = res.value;
+        if (client.isMobile) return this.touchScroll(elem.ELEMENT, offsetX, offsetY);
+        return this.elementIdLocation(elem.ELEMENT).then(function (location) {
+          if (!location.value || location.value.length === 0) {
+            throw new Error(
+            `Failed to recieve (${locator}) location`);
+          }
+          return this.execute(function scroll(x, y) {
+            return window.scrollTo(x, y);
+          }, location.value.x + offsetX, location.value.y + offsetY);
+        });
+      });
+    } else {
+      if (client.isMobile) return client.touchScroll(locator, offsetX, offsetY);
+      return this.execute(function scroll(x, y) {
+        return window.scrollTo(x, y);
+      }, offsetX, offsetY);
+    }
   }
 
   /**
@@ -923,7 +1035,39 @@ class WebDriverIO extends Helper {
    * Appium: support only web testing
    */
   moveCursorTo(locator, offsetX, offsetY) {
-    return this.browser.moveToObject(withStrictLocator(locator), offsetX, offsetY);
+    let client = this.browser;
+    var hasOffsetParams = true;
+    if (typeof offsetX !== 'number' && typeof offsetY !== 'number') {
+      hasOffsetParams = false;
+    }
+
+    return client.element(withStrictLocator(locator)).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        return truth(`elements of ${locator}`, 'to be seen').assert(false);
+      }
+      let elem = res.value;
+      if (client.isMobile) {
+        return this.elementIdSize(elem.ELEMENT).then(function (size) {
+          if (!size.value || size.value.length === 0) throw new Error(`Failed to recieve (${locator}) size`);
+          return this.elementIdLocation(elem.ELEMENT).then(function (location) {
+            if (!location.value || location.value.length === 0) {
+              throw new Error(
+              `Failed to recieve (${locator}) location`);
+            }
+            var x = location.value.x + size.value.width / 2;
+            var y = location.value.y + size.value.height / 2;
+
+            if (hasOffsetParams) {
+              x = location.value.x + offsetX;
+              y = location.value.y + offsetY;
+            }
+            return this.touchMove(x, y);
+          });
+        });
+      }
+
+      return this.moveTo(elem.ELEMENT, offsetX, offsetY);
+    });
   }
 
   /**
@@ -943,9 +1087,9 @@ class WebDriverIO extends Helper {
         width: document.body.scrollWidth
       };
     }).then(({
-      width,
-      height
-    }) => {
+               width,
+               height
+             }) => {
       this.browser.windowHandleSize(width, height);
       this.debug('Screenshot has been saved to ' + outputFile);
       return this.browser.saveScreenshot(outputFile);
@@ -957,7 +1101,8 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/setCookie}}
    * Appium: support only web testing
    *
-   * Uses Selenium's JSON [cookie format](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object).
+   * Uses Selenium's JSON [cookie
+   * format](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object).
    */
   setCookie(cookie) {
     return this.browser.setCookie(cookie);
@@ -1103,12 +1248,56 @@ class WebDriverIO extends Helper {
    * ```
    */
   dragAndDrop(srcElement, destElement) {
-    return this.browser.dragAndDrop(
-      withStrictLocator(srcElement),
-      withStrictLocator(destElement)
-    );
+    let client = this.browser;
+    let this2 = this;
+
+
+    if (client.isMobile) {
+      return client.element(withStrictLocator(srcElement)).then(function (res) {
+        if (!res.value || res.value.length === 0) return truth(`elements of ${srcElement}`, 'to be seen').assert(false);
+        let elem = res.value;
+        return this.elementIdLocation(elem.ELEMENT).then(function (location) {
+          if (!location.value || location.value.length === 0) {
+            throw new Error(
+            `Failed to recieve (${srcElement}) location`);
+          }
+          return this.touchDown(location.value.x, location.value.y).then(function (res) {
+            if (res.state !== 'success') throw new Error(`Failed to touch button down on (${srcElement})`);
+            return client.element(withStrictLocator(destElement)).then(function (res) {
+              if (!res.value || res.value.length === 0) {
+                return truth(`elements of ${destElement}`, 'to be seen')
+                .assert(false);
+              }
+              elem = res.value;
+              return this.elementIdLocation(elem.ELEMENT).then(function (location) {
+                if (!location.value || location.value.length === 0) {
+                  throw new Error(
+                  `Failed to recieve (${destElement}) location`);
+                }
+                return this.touchMove(location.value.x, location.value.y).then(function (res) {
+                  if (res.state !== 'success') throw new Error(`Failed to touch move to (${destElement})`);
+                  return this.touchUp(location.value.x, location.value.y);
+                });
+              });
+            });
+          });
+        });
+      });
+    }
+
+    return this2.moveCursorTo(withStrictLocator(srcElement)).then(function (res) {
+      if (res.state !== 'success') throw new Error(`Unable to move cursor to (${srcElement})`);
+      return this.buttonDown().then(function (res) {
+        if (res.state !== 'success') throw new Error(`Failed to press button down on (${srcElement})`);
+        return this2.moveCursorTo(withStrictLocator(destElement)).then(function (res) {
+          if (res.state !== 'success') throw new Error(`Unable to move cursor to (${destElement})`);
+          return this.buttonUp();
+        });
+      });
+    });
   }
 
+
   /**
    * Close all tabs expect for one.
    * Appium: support web test
@@ -1140,8 +1329,25 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForEnabled(locator, sec) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitForEnabled(withStrictLocator(locator), sec * 1000);
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
+        }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdEnabled(el.ELEMENT)));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (Array.isArray(selected)) {
+            return selected.filter((val) => val === true).length > 0;
+          }
+          return selected;
+        });
+      });
+    }, sec * 1000, `element (${locator}) still not enabled after ${sec} sec`);
   }
 
   /**
@@ -1149,8 +1355,15 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForElement(locator, sec) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitForExist(withStrictLocator(locator), sec * 1000);
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
+        } else return true;
+      });
+    }, sec * 1000, `element (${locator}) still not present on page after ${sec} sec`);
   }
 
   /**
@@ -1158,23 +1371,27 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForText(text, sec, context) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
     context = context || this.root;
-    return this.browser.waitUntil(function () {
-      return this.getText(withStrictLocator(context)).then(function (source) {
-        if (Array.isArray(source)) {
-          return source.filter(part => part.indexOf(text) >= 0).length > 0;
-        }
-        return source.indexOf(text) >= 0;
-      });
-    }, sec * 1000)
-      .catch((e) => {
-        if (e.type === 'WaitUntilTimeoutError') {
-          return proceedSee.call(this, 'assert', text, withStrictLocator(context));
-        } else {
-          throw e;
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(context)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
         }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdText(el.ELEMENT)));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (Array.isArray(selected)) {
+            return selected.filter(part => part.indexOf(text) >= 0).length > 0;
+          }
+          return selected.indexOf(text) >= 0;
+        });
       });
+    }, sec * 1000,
+      `element (${context}) is not in DOM or there is no element(${context}) with text "${text}" after ${sec} sec`);
   }
 
   /**
@@ -1182,8 +1399,25 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForVisible(locator, sec) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitForVisible(withStrictLocator(locator), sec * 1000);
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
+        }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (Array.isArray(selected)) {
+            return selected.filter((val) => val === true).length > 0;
+          }
+          return selected;
+        });
+      });
+    }, sec * 1000, `element (${locator}) still not visible after ${sec} sec`);
   }
 
   /**
@@ -1191,8 +1425,25 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForInvisible(locator, sec) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitForVisible(withStrictLocator(locator), sec * 1000, true);
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return true;
+        }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (Array.isArray(selected)) {
+            return selected.filter((val) => val === false).length > 0;
+          }
+          return !selected;
+        });
+      });
+    }, sec * 1000, `element (${locator}) still visible after ${sec}sec`);
   }
 
   /**
@@ -1209,17 +1460,24 @@ class WebDriverIO extends Helper {
    * Appium: support
    */
   waitForStalenessOf(locator, sec) {
+    let client = this.browser;
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitForExist(withStrictLocator(locator), sec * 1000, true);
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return true;
+        } else return false;
+      });
+    }, sec * 1000, `element (${locator}) still attached to the DOM after ${sec}sec`);
   }
 
   /**
    * Waits for a function to return true (waits for 1sec by default).
    * Appium: support
    */
-  waitUntil(fn, sec) {
+  waitUntil(fn, sec, timeoutMsg) {
     sec = sec || this.options.waitForTimeout;
-    return this.browser.waitUntil(fn, sec);
+    return this.browser.waitUntil(fn, sec, timeoutMsg);
   }
 
   /**
@@ -1228,7 +1486,14 @@ class WebDriverIO extends Helper {
    */
   switchTo(locator) {
     locator = locator || null;
-    return this.browser.frame(locator);
+    if (Number.isInteger(locator)) return this.browser.frame(locator);
+    if (typeof locator === 'undefined' || locator === null) return this.browser.frame(null);
+    return this.browser.element(withStrictLocator(locator)).then(function (res) {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`Element ${locator} not found by name|text|CSS|XPath`);
+      }
+      return this.frame(res.value);
+    });
   }
 
 }
@@ -1236,17 +1501,31 @@ class WebDriverIO extends Helper {
 function proceedSee(assertType, text, context) {
   let description;
   if (!context) {
-    context = this.context;
     if (this.context === webRoot) {
+      context = this.context;
       description = 'web page';
     } else {
       description = 'current context ' + this.context;
+      context = './/*';
     }
   } else {
     description = 'element ' + context;
   }
-  return this.browser.getText(withStrictLocator(context)).then(function (source) {
-    return stringIncludes(description)[assertType](text, source);
+
+  let client = this.browser;
+
+  return client.elements(withStrictLocator(context)).then(function (res) {
+    if (!res.value || res.value.length === 0) {
+      throw new Error(`${context} can't be located by name|text|CSS|XPath`);
+    }
+
+    let commands = [];
+    res.value.forEach((el) => commands.push(this.elementIdText(el.ELEMENT)));
+    return this.unify(commands, {
+      extractValue: true
+    }).then((selected) => {
+      return stringIncludes(description)[assertType](text, selected);
+    });
   });
 }
 
@@ -1315,7 +1594,7 @@ function proceedSeeField(assertType, field, value) {
     var proceedMultiple = (fields) => {
       let commands = [];
       fields.forEach((el) => commands.push(this.elementIdSelected(el.ELEMENT)));
-      this.unify(commands).then((res) => {
+      this.unify(commands).then(() => {
         commands = [];
         fields.forEach((el) => {
           if (el.value === false) return;
@@ -1336,13 +1615,13 @@ function proceedSeeField(assertType, field, value) {
     };
 
     return this.elementIdName(res.value[0].ELEMENT).then((tag) => {
-      if (tag.value == 'select') {
+      if (tag.value === 'select') {
         return proceedMultiple(res.value);
       }
 
-      if (tag.value == 'input') {
+      if (tag.value === 'input') {
         return this.elementIdAttribute(res.value[0].ELEMENT, 'type').then((type) => {
-          if (type.value == 'checkbox' || type.value == 'radio') {
+          if (type.value === 'checkbox' || type.value === 'radio') {
             return proceedMultiple(res.value);
           }
           return proceedSingle(res.value[0]);
diff --git a/lib/recorder.js b/lib/recorder.js
index 2a69345d..3948c178 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -1,6 +1,6 @@
 'use strict';
 
-let promise, oldPromise;
+let promise;
 let running = false;
 let errFn;
 let next;
@@ -10,6 +10,8 @@ let sessionId = null;
 let log = require('./output').log;
 let tasks = [];
 
+let oldPromises = [];
+
 /**
  * Singleton object to record all test steps as promises and run them in chain.
  */
@@ -58,7 +60,7 @@ module.exports = {
     sessionId = null;
     log(currentQueue() + `Starting recording promises`);
     promise = Promise.resolve();
-    oldPromise = null;
+    oldPromises = [];
     tasks = [];
     this.session.running = false;
   },
@@ -69,18 +71,18 @@ module.exports = {
     start(name) {
       log(currentQueue() + `Starting <${name}> session`);
       tasks.push('--->');
-      oldPromise = promise;
+      oldPromises.push(promise);
       this.running = true;
       sessionId = name;
       promise = Promise.resolve();
     },
 
-    restore() {
+    restore(name) {
       tasks.push('<---');
-      log(currentQueue() + `Starting <${this.name}> session`);
+      log(currentQueue() + `Finalize <${name}> session`);
       this.running = false;
       sessionId = null;
-      promise = promise.then(() => oldPromise);
+      promise = promise.then(() => oldPromises.pop());
     },
 
     catch(errFn) {
diff --git a/lib/scenario.js b/lib/scenario.js
index aae5a8cd..6181ac3c 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -3,6 +3,28 @@ let event = require('./event');
 let container = require('./container');
 let recorder = require('./recorder');
 let getParamNames = require('./utils').getParamNames;
+let isGenerator = require('./utils').isGenerator;
+
+
+var resumeTest = module.exports.resumeTest = function (res, done, error, returnStatement) {
+  recorder.add('create new promises queue for generator', function (data) {
+    recorder.session.start('generator'); // creating a new promise chain
+    try {
+      let resume = res.next(data);
+      if (resume.done) {
+        done();
+      } else {
+        resumeTest(res, done, error, returnStatement);
+      }
+    } catch (err) {
+      recorder.throw(err); // catch error in test and rethrow into promise
+    }
+    recorder.catch(error); // handle all errors in promise chain
+    if (returnStatement) {
+      return recorder.promise();
+    }
+  });
+};
 
 /**
  * Wraps test function, injects support objects from container,
@@ -36,24 +58,10 @@ module.exports.test = (test) => {
           return test;
         }
         recorder.catch(); // catching possible errors in promises
-        let resumeTest = function () {
-          recorder.add('create new promises queue for generator', function (data) {
-            recorder.session.start('generator'); // creating a new promise chain
-            try {
-              let resume = res.next(data);
-              if (resume.done) {
-                recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
-                recorder.add('finish generator with no error', () => done()); // finish him
-              } else {
-                resumeTest();
-              }
-            } catch (err) {
-              recorder.throw(err); // catch error in test and rethrow into promise
-            }
-            recorder.catch(); // handle all errors in promise chain
-          });
-        };
-        resumeTest();
+        resumeTest(res, () => {
+          recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
+          recorder.add('finish generator with no error', () => done()); // finish him
+        });
       }
     } catch (err) {
       recorder.throw(err);
@@ -112,10 +120,6 @@ module.exports.suiteTeardown = function (suite) {
   event.emit(event.suite.after, suite);
 };
 
-function isGenerator(fn) {
-  return fn.constructor.name === 'GeneratorFunction';
-}
-
 function getInjectedArguments(fn) {
   let testArguments = [];
   let params = getParamNames(fn) || [];
diff --git a/lib/utils.js b/lib/utils.js
index f3beb20c..33ff237f 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -22,6 +22,10 @@ function deepMerge(target, source) {
 
 module.exports.deepMerge = deepMerge;
 
+module.exports.isGenerator = function (fn) {
+  return fn.constructor.name === 'GeneratorFunction';
+};
+
 module.exports.fileExists = function (filePath) {
   try {
     fs.statSync(filePath);
diff --git a/lib/within.js b/lib/within.js
index e7a45464..858e0425 100644
--- a/lib/within.js
+++ b/lib/within.js
@@ -3,6 +3,8 @@ let output = require('./output');
 let recorder = require('./recorder');
 let container = require('./container');
 let event = require('./event');
+let isGenerator = require('./utils').isGenerator;
+let resumeTest = require('./scenario').resumeTest;
 
 function within(context, fn) {
   recorder.add('register within wrapper', function () {
@@ -21,14 +23,50 @@ function within(context, fn) {
       if (helpers[helper]._withinBegin) recorder.add('start within block', () => helpers[helper]._withinBegin(context));
     });
 
-    fn();
 
-    Object.keys(helpers).forEach((helper) => {
-      if (helpers[helper]._withinEnd) recorder.add('finish within block', () => helpers[helper]._withinEnd());
-    });
-
-    recorder.add('restore session', () => {
-      recorder.session.restore();
+    try {
+      let res = fn.apply();
+      if (isGenerator(fn)) {
+        try {
+          res.next();
+        } catch (err) {
+          event.emit(event.test.failed, test, err);
+          recorder.throw(err);
+        }
+        recorder.catch((err) => {
+          output.stepShift = 1;
+          event.dispatcher.removeListener(event.step.after, addContextToStep);
+          throw err;
+        }); // catching possible errors in promises
+        resumeTest(res, () => {
+          Object.keys(helpers).forEach((helper) => {
+            if (helpers[helper]._withinEnd) recorder.add('finish within block', () => helpers[helper]._withinEnd());
+          });
+          recorder.add('Finalize session within_generator', () => {
+            recorder.session.restore('within');
+          });
+        }, (err) => {
+          output.stepShift = 1;
+          event.dispatcher.removeListener(event.step.after, addContextToStep);
+          throw err;
+        }, true);
+      }
+    } catch (err) {
+      recorder.throw(err);
+    } finally {
+      if (!isGenerator(fn)) {
+        Object.keys(helpers).forEach((helper) => {
+          if (helpers[helper]._withinEnd) recorder.add('finish within block', () => helpers[helper]._withinEnd());
+        });
+        recorder.catch((err) => {
+          output.stepShift = 1;
+          event.dispatcher.removeListener(event.step.after, addContextToStep);
+          throw err;
+        });
+      }
+    }
+    recorder.add('Finilize within session', () => {
+      recorder.session.restore('within');
       output.stepShift = 1;
       event.dispatcher.removeListener(event.step.after, addContextToStep);
     });


Test patch:
diff --git a/test/data/sandbox/base_test_within.js b/test/data/sandbox/base_test_within.js
new file mode 100644
index 00000000..3ae53d36
--- /dev/null
+++ b/test/data/sandbox/base_test_within.js
@@ -0,0 +1,65 @@
+Feature('Within');
+
+Scenario('Check within without generator', (I) => {
+  I.smallPromise();
+  within('blabla', () => {
+    I.smallPromise();
+  });
+});
+
+Scenario('Check within with generator. Yield is first in order', function* (I){
+  I.smallPromise();
+  let test = yield I.smallYield();
+  console.log(test);
+  within('blabla', function* () {
+    let testwithin = yield I.smallYield();
+    console.log(testwithin);
+    I.smallPromise();
+  });
+});
+
+Scenario('Check within with generator. Yield is second in order', function* (I){
+  I.smallPromise();
+  let test = yield I.smallYield();
+  console.log(test)
+  within('blabla', function* () {
+    I.smallPromise();
+    let testwithin = yield I.smallYield();
+    console.log(testwithin);
+  });
+});
+
+Scenario('Check within with generator. Should complete test steps after within', function* (I){
+  let test = yield I.smallYield();
+  console.log(test);
+  within('blabla', function* () {
+    let testwithin = yield I.smallYield();
+    console.log(testwithin);
+    I.smallPromise();
+  });
+  I.smallPromise();
+});
+
+Scenario('Check within with generator. Should stop test execution after fail in within', function* (I){
+  let test = yield I.smallYield();
+  console.log(test);
+  within('blabla', function* () {
+    I.errorStep();
+    let testwithin = yield I.smallYield();
+    console.log(testwithin);
+    I.smallPromise();
+  });
+  I.smallPromise();
+});
+
+Scenario('Check within with generator. Should stop test execution after fail in main block', function* (I){
+  I.errorStep();
+  within('blabla', function* () {
+    I.errorStep();
+    let testwithin = yield I.smallYield();
+    console.log(testwithin);
+    I.smallPromise();
+  });
+  I.smallPromise();
+});
+
diff --git a/test/data/sandbox/codecept.within.json b/test/data/sandbox/codecept.within.json
new file mode 100644
index 00000000..81c64e9d
--- /dev/null
+++ b/test/data/sandbox/codecept.within.json
@@ -0,0 +1,14 @@
+{
+  "tests": "./*test_within.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "Within": {
+      "require": "./within_helper"
+    }
+  },
+  "include": {},
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
diff --git a/test/data/sandbox/within_helper.js b/test/data/sandbox/within_helper.js
new file mode 100644
index 00000000..7a9fe6b1
--- /dev/null
+++ b/test/data/sandbox/within_helper.js
@@ -0,0 +1,37 @@
+'use strict';
+let Helper = require('../../../lib/helper');
+let output = require('../../../lib/output');
+
+class Whithin extends Helper {
+
+  _withinBegin(testStr) {
+    output.step(`Hey! I am within Begin. I get ${testStr}`);
+  }
+
+  _withinEnd() {
+    output.step(`oh! I am within end(`);
+  }
+
+  _failed() {
+    this._withinEnd();
+  }
+
+  smallYield() {
+    return `I am small yield string`;
+  }
+
+  smallPromise() {
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        resolve("result");
+      }, 100);
+    }).then(() => output.step("small Promise was finished"));
+  }
+
+  errorStep() {
+    throw new Error('ups, error');
+  }
+
+}
+
+module.exports = Whithin;
diff --git a/test/helper/AppiumWeb_test.js b/test/helper/AppiumWeb_test.js
new file mode 100644
index 00000000..40a7b791
--- /dev/null
+++ b/test/helper/AppiumWeb_test.js
@@ -0,0 +1,178 @@
+'use strict';
+
+let Appium = require('../../lib/helper/Appium');
+var chai = require('chai');
+let should = require('chai').should();
+var chaiAsPromised = require('chai-as-promised');
+chai.use(chaiAsPromised);
+var expect = chai.expect;
+let I;
+let site_url = 'http://davertmik.github.io/angular-demo-app';
+let assert = require('assert');
+let path = require('path');
+let fs = require('fs');
+let fileExists = require('../../lib/utils').fileExists;
+let AssertionFailedError = require('../../lib/assert/error');
+let expectError = require('../../lib/utils').test.expectError;
+let webApiTests = require('./webapi');
+let within = require('../../lib/within')
+require('co-mocha')(require('mocha'));
+
+describe('Appium Web', function () {
+  this.retries(4);
+  this.timeout(70000);
+
+  before(() => {
+    I = new Appium({
+      url: site_url,
+      browser: 'chrome',
+      desiredCapabilities: {
+        appiumVersion: "1.6.4",
+        recordVideo: "false",
+        recordScreenshots: "false",
+        platformName: "Android",
+        platformVersion: "6.0",
+        deviceName: "Android Emulator"
+      },
+      host: 'ondemand.saucelabs.com',
+      port: 80,
+      // port: 4723,
+      // host: 'localhost',
+      user: process.env.SAUCE_USERNAME,
+      key: process.env.SAUCE_ACCESS_KEY
+    });
+  });
+
+  beforeEach(() => {
+    I.isWeb = true;
+    return I._before();
+  });
+
+  afterEach(() => {
+    return I._after();
+  });
+
+  describe('current url : #seeInCurrentUrl, #seeCurrentUrlEquals, ...', () => {
+    it('should check for url fragment', function*() {
+      yield I.amOnPage(site_url+'/#/info');
+      yield I.seeInCurrentUrl('/info');
+      return I.dontSeeInCurrentUrl('/result');
+    });
+
+    it('should check for equality', function*() {
+      yield I.amOnPage('/#/info');
+      yield I.seeCurrentUrlEquals('/#/info');
+      return I.dontSeeCurrentUrlEquals('/#/result');
+    });
+  });
+
+  describe('see text : #see', () => {
+    it('should check text on site', function*() {
+      yield I.amOnPage('/');
+      yield I.see('Description');
+      return I.dontSee('Create Event Today');
+    });
+
+    it('should check text inside element', function*() {
+      yield I.amOnPage('/#/info');
+      yield I.see('About', 'h1');
+      yield I.see('Welcome to event app', {css: 'p.jumbotron'});
+      return I.see('Back to form', '//div/a');
+    });
+  });
+
+  describe('see element : #seeElement, #dontSeeElement', () => {
+    it('should check visible elements on page', function*() {
+      yield I.amOnPage('/');
+      yield I.seeElement('.btn.btn-primary');
+      yield I.seeElement({css: '.btn.btn-primary'});
+      return I.dontSeeElement({css: '.btn.btn-secondary'});
+    });
+  });
+
+
+  describe('#click', () => {
+    it('should click by text', function*() {
+      yield I.amOnPage('/');
+      yield I.dontSeeInCurrentUrl('/info');
+      yield I.click('Get more info!');
+      return I.seeInCurrentUrl('/info');
+    });
+
+    it('should click by css', function*() {
+      yield I.amOnPage('/');
+      yield I.click('.btn-primary');
+      yield I.wait(2);
+      return I.seeInCurrentUrl('/result');
+    });
+
+    it('should click by non-optimal css', function*() {
+      yield I.amOnPage('/');
+      yield I.click('form a.btn');
+      yield I.wait(2);
+      return I.seeInCurrentUrl('/result');
+    });
+
+    it('should click by xpath', function*() {
+      yield I.amOnPage('/');
+      yield I.click('//a[contains(., "more info")]');
+      return I.seeInCurrentUrl('/info');
+    });
+
+    it('should click on context', function*() {
+      yield I.amOnPage('/');
+      yield I.click('.btn-primary', 'form');
+      yield I.wait(2);
+      return I.seeInCurrentUrl('/result');
+    });
+
+    it('should click link with inner span', function*() {
+      yield I.amOnPage('/#/result');
+      yield I.click('Go to info');
+      return I.seeInCurrentUrl('/info')
+    });
+
+    it('should click buttons as links', function*() {
+      yield I.amOnPage('/');
+      yield I.click('Options');
+      return I.seeInCurrentUrl('/options')
+    });
+  });
+
+  describe('#grabTextFrom, #grabValueFrom, #grabAttributeFrom', () => {
+    it('should grab text from page', function*() {
+      yield I.amOnPage('/#/info');
+      let val = yield I.grabTextFrom('p.jumbotron');
+      return expect(val).to.equal('Welcome to event app');
+    });
+
+    it('should grab value from field', function*() {
+      yield I.amOnPage('/#/options');
+      let val = yield I.grabValueFrom('#ssh');
+      return expect(val).to.equal('PUBLIC-SSH-KEY');
+    });
+
+    it('should grab attribute from element', function*() {
+      yield I.amOnPage('/#/info');
+      let val = yield I.grabAttributeFrom('a.btn', 'ng-href');
+      return expect(val).to.equal('#/');
+    });
+  });
+
+  xdescribe('#within', () => {
+
+    afterEach(() => I._withinEnd());
+
+    it('should work using within operator', function*() {
+      yield I.see('This is a very simple event creating app built with AngularJS');
+      yield I.amOnPage('/#/info');
+      // yield I.wait(2);
+      yield I._withinBegin({ css: 'p.jumbotron'});
+      yield I.see('Welcome to event app');
+    //   yield I.see('About');
+    //   return I.dontSee('This is a very simple event creating app built with AngularJS');
+    });
+
+  });
+
+});
diff --git a/test/helper/Appium_test.js b/test/helper/Appium_test.js
index 5508ae11..51a7578f 100644
--- a/test/helper/Appium_test.js
+++ b/test/helper/Appium_test.js
@@ -19,12 +19,6 @@ describe('Appium', function () {
   this.timeout(0);
 
   before(() => {
-    global.codecept_dir = path.join(__dirname, '/../data');
-    try {
-      fs.unlinkSync(dataFile);
-    } catch (err) {
-    }
-
     app = new Appium({
       app: apk_path,
       desiredCapabilities: {
diff --git a/test/helper/Protractor_test.js b/test/helper/Protractor_test.js
index ab39964f..b6674cbf 100644
--- a/test/helper/Protractor_test.js
+++ b/test/helper/Protractor_test.js
@@ -36,7 +36,7 @@ describe('Protractor', function() {
   });
 
   after(() => {
-    return I._afterSuite();
+    return I._finishTest();
   });
 
   beforeEach(() => {
@@ -377,7 +377,7 @@ describe('Protractor', function() {
       yield I.saveScreenshot('protractor_user_full.png',true);
       return assert.ok(fileExists(path.join(output_dir, 'protractor_user_full.png')), null, 'file does not exists');
     });
-    
+
     it('should create a screenshot on fail', function*() {
       let test = { title: 'protractor should do smth' };
       yield I.amOnPage('/')
diff --git a/test/helper/SeleniumWebdriver_test.js b/test/helper/SeleniumWebdriver_test.js
index b8debcdd..e81bc96a 100644
--- a/test/helper/SeleniumWebdriver_test.js
+++ b/test/helper/SeleniumWebdriver_test.js
@@ -26,14 +26,16 @@ describe('SeleniumWebdriver', function () {
     I = new SeleniumWebdriver({
       url: site_url,
       browser: 'chrome',
-      windowSize: '500x400'
+      windowSize: '500x400',
+      restart: false
+
     });
     I._init();
-    browser = I._before();
+    return browser = I._beforeSuite();
   });
 
   after(function() {
-    I._afterSuite();
+    return I._finishTest();
   });
 
   beforeEach(function() {
diff --git a/test/helper/WebDriverIO_test.js b/test/helper/WebDriverIO_test.js
index ff9802d4..b1690570 100644
--- a/test/helper/WebDriverIO_test.js
+++ b/test/helper/WebDriverIO_test.js
@@ -12,6 +12,7 @@ let AssertionFailedError = require('../../lib/assert/error');
 let formContents = require('../../lib/utils').test.submittedData(path.join(__dirname, '/../data/app/db'));
 let expectError = require('../../lib/utils').test.expectError;
 let webApiTests = require('./webapi');
+let within = require('../../lib/within')
 
 
 describe('WebDriverIO', function () {
@@ -79,7 +80,7 @@ describe('WebDriverIO', function () {
           e.inspect().should.include('web page');
       });
     });
-  });
+      });
 
   describe('check fields: #seeInField, #seeCheckboxIsChecked, ...', () => {
     it('should throw error if field is not empty', () => {
@@ -195,8 +196,7 @@ describe('WebDriverIO', function () {
         .then(() => wd.waitForText('Nothing here', 1, '#text'))
         .then(expectError)
         .catch((e) => {
-          e.should.be.instanceOf(AssertionFailedError);
-          e.inspect().should.be.equal('expected element #text to include "Nothing here"');
+          e.message.should.be.equal('element (#text) is not in DOM or there is no element(#text) with text "Nothing here" after 1 sec');
         });
     });
 
@@ -205,8 +205,7 @@ describe('WebDriverIO', function () {
         .then(() => wd.waitForText('Dynamic text', 0.1))
         .then(expectError)
         .catch((e) => {
-          e.should.be.instanceOf(AssertionFailedError);
-          e.inspect().should.be.equal('expected element body to include "Dynamic text"');
+          e.message.should.be.equal('element (body) is not in DOM or there is no element(body) with text "Dynamic text" after 0.1 sec');
         });
     });
   });
@@ -221,7 +220,7 @@ describe('WebDriverIO', function () {
   describe('#switchTo', () => {
       it('should switch reference to iframe content', () => {
           return wd.amOnPage('/iframe')
-            .then(() => wd.switchTo('content'))
+            .then(() => wd.switchTo('[name="content"]'))
             .then(() => wd.see('Information\nLots of valuable data here'));
       });
 
@@ -231,13 +230,23 @@ describe('WebDriverIO', function () {
             .then(expectError)
             .catch((e) => {
                 e.should.be.instanceOf(Error);
-                e.seleniumStack.type.should.be.equal('NoSuchFrame');
+                e.message.should.be.equal('Element #invalidIframeSelector not found by name|text|CSS|XPath');
             });
       });
 
+    it('should return error if iframe selector is not iframe', () => {
+      return wd.amOnPage('/iframe')
+        .then(() => wd.switchTo('h1'))
+        .then(expectError)
+        .catch((e) => {
+          e.should.be.instanceOf(Error);
+          e.seleniumStack.type.should.be.equal('NoSuchFrame');
+        });
+    });
+
       it('should return to parent frame given a null locator', () => {
         return wd.amOnPage('/iframe')
-          .then(() => wd.switchTo('content'))
+          .then(() => wd.switchTo('[name="content"]'))
           .then(() => wd.see('Information\nLots of valuable data here'))
           .then(() => wd.switchTo(null))
           .then(() => wd.see('Iframe test'));
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index c24fb721..4fe38c9a 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -496,7 +496,7 @@ module.exports.tests = function() {
         .then(() => assert.ok(fileExists(path.join(output_dir, `screenshot_full_${+sec}`)), null, 'file does not exists'));
     });
 
-    it('should create a screenshot on fail', () => {
+    it('should create a screenshot on fail  @ups', () => {
       let sec = (new Date()).getUTCMilliseconds().toString();
       let test = { title: 'sw should do smth '+sec };
       return I.amOnPage('/')
@@ -526,7 +526,7 @@ module.exports.tests = function() {
   });
 
   describe('#waitForText', () => {
-    it('should wait for text', () => {
+    it('should wait for text @ups', () => {
       return I.amOnPage('/dynamic')
         .then(() => I.dontSee('Dynamic text'))
         .then(() => I.waitForText('Dynamic text', 2))
@@ -577,6 +577,68 @@ module.exports.tests = function() {
     });
 
 
-  })
+  });
+
+  describe('within tests', () => {
+
+    afterEach(() => {
+      return I._withinEnd();
+    });
+
+    it('should execute within block', () => {
+      return I.amOnPage('/form/example4')
+        .then(() => I.seeElement('#navbar-collapse-menu'))
+        .then(() => I._withinBegin('#register'))
+        .then(() => I.see('E-Mail'))
+        .then(() => I.dontSee('Toggle navigation'))
+        .then(() => I.dontSeeElement('#navbar-collapse-menu'))
+    });
+
+
+    it('should respect form fields inside within block ', () => {
+      let rethrow;
+      return I.amOnPage('/form/example4')
+        .then(() => I.seeElement('#navbar-collapse-menu'))
+        .then(() => I.see('E-Mail'))
+        .then(() => I.see('Hasło'))
+        .then(() => I.fillField('Hasło', '12345'))
+        .then(() => I.seeInField('Hasło', '12345'))
+        .then(() => I.checkOption('terms'))
+        .then(() => I.seeCheckboxIsChecked('terms'))
+        .then(() => I._withinBegin({css: '.form-group'}))
+        .then(() => I.see('E-Mail'))
+        .then(() => I.dontSee('Hasło'))
+        .then(() => I.dontSeeElement('#navbar-collapse-menu'))
+        .catch((err) => rethrow = err)
+        .then(() => I.dontSeeCheckboxIsChecked('terms'))
+        .catch((err) => { if (!err) assert.fail('seen checkbox') })
+        .then(() => I.seeInField('Hasło', '12345'))
+        .catch((err) => { if (!err) assert.fail('seen field') })
+        .then(() => { if (rethrow) throw rethrow });
+    });
+
+
+    it('should execute within block ', () => {
+      return I.amOnPage('/form/example4')
+        .then(() => I.fillField('Hasło', '12345'))
+        .then(() => I._withinBegin({xpath: '//div[@class="form-group"][2]'}))
+        .then(() => I.dontSee('E-Mail'))
+        .then(() => I.see('Hasło'))
+        .then(() => I.grabTextFrom('label'))
+        .then((label) => assert.equal(label, 'Hasło'))
+        .then(() => I.grabValueFrom('input'))
+        .then((input) => assert.equal(input, '12345'));
+    });
+
+    it('within should respect context in see', () => {
+      return I.amOnPage('/form/example4')
+        .then(() => I.see('Rejestracja', 'fieldset'))
+        .then(() => I._withinBegin({css: '.navbar-header'}))
+        .then(() => I.see('Rejestracja', '.container fieldset'))
+        .catch((err) => { if (!err) assert.fail('seen fieldset') })
+        .then(() => I.see('Toggle navigation', '.container fieldset'))
+        .catch((err) => { if (!err) assert.fail('seen fieldset') })
+    });
+  });
 
 }
diff --git a/test/runner/within_test.js b/test/runner/within_test.js
new file mode 100644
index 00000000..a2666d5e
--- /dev/null
+++ b/test/runner/within_test.js
@@ -0,0 +1,174 @@
+'use strict';
+let should = require('chai').should();
+let assert = require('assert');
+let path = require('path');
+const exec = require('child_process').exec;
+let runner = path.join(__dirname, '/../../bin/codecept.js');
+let codecept_dir = path.join(__dirname, '/../data/sandbox')
+let codecept_run = runner +' run --config '+codecept_dir + '/codecept.within.json ';
+let fs;
+
+let getlines = function (array, startString, endString) {
+  let startIndex, endIndex;
+  array.every(function (elem, index) {
+    if (elem === startString) {
+      startIndex = index;
+      return true;
+    }
+    if (elem === endString) {
+      endIndex = index;
+      return false;
+    }
+    return true;
+  })
+  return array.slice(startIndex + 1, endIndex);
+}
+let testStatus;
+
+describe('CodeceptJS within', function () {
+
+  this.timeout(40000);
+
+  before(() => {
+    global.codecept_dir = path.join(__dirname, '/../data/sandbox');
+  });
+
+  it('should execute if no generators', (done) => {
+    exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+      let lines = stdout.match(/\S.+/g);
+
+      let withoutGeneratorList = getlines(lines, 'Check within without generator', 'Check within with generator. Yield is first in order');
+      testStatus = withoutGeneratorList.pop();
+      testStatus.should.include('OK');
+      withoutGeneratorList.should.eql([
+        '• I small promise ',
+        '• small Promise was finished',
+        'Within blabla:',
+        '• Hey! I am within Begin. I get blabla',
+        '• Within blabla: I small promise ',
+        '• small Promise was finished',
+        '• oh! I am within end('
+      ], 'check steps execution order');
+      done();
+    });
+  });
+
+it('should execute with generators. Yield is first in order', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    let lines = stdout.match(/\S.+/g);
+
+    let withGeneratorList = getlines(lines, 'Check within with generator. Yield is first in order', 'Check within with generator. Yield is second in order');
+    testStatus = withGeneratorList.pop();
+    testStatus.should.include('OK');
+    withGeneratorList.should.eql([
+      "• I small promise ",
+      "• small Promise was finished",
+      "• I small yield ",
+      "I am small yield string",
+      "Within blabla:",
+      "• Hey! I am within Begin. I get blabla",
+      "• Within blabla: I small yield ",
+      "I am small yield string",
+      "• Within blabla: I small promise ",
+      "• small Promise was finished",
+      "• oh! I am within end("
+    ], 'check steps execution order');
+
+    done();
+  });
+});
+
+it('should execute with generators. Yield is second in order', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    let lines = stdout.match(/\S.+/g);
+
+    let withGeneratorListOtherOrder = getlines(lines, 'Check within with generator. Yield is second in order', 'Check within with generator. Should complete test steps after within');
+    testStatus = withGeneratorListOtherOrder.pop();
+    testStatus.should.include('OK');
+    withGeneratorListOtherOrder.should.eql([
+      "• I small promise ",
+      "• small Promise was finished",
+      "• I small yield ",
+      "I am small yield string",
+      "Within blabla:",
+      "• Hey! I am within Begin. I get blabla",
+      "• Within blabla: I small promise ",
+      "• small Promise was finished",
+      "• Within blabla: I small yield ",
+      "I am small yield string",
+      "• oh! I am within end("
+    ], 'check steps execution order');
+
+    done();
+  });
+});
+
+it('should complete test steps after within', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    let lines = stdout.match(/\S.+/g);
+
+    let withGeneratorListWithContinued = getlines(lines, 'Check within with generator. Should complete test steps after within', 'Check within with generator. Should stop test execution after fail in within');
+    testStatus = withGeneratorListWithContinued.pop();
+    testStatus.should.include('OK');
+    withGeneratorListWithContinued.should.eql([
+      "• I small yield ",
+      "I am small yield string",
+      "Within blabla:",
+      "• Hey! I am within Begin. I get blabla",
+      "• Within blabla: I small yield ",
+      "I am small yield string",
+      "• Within blabla: I small promise ",
+      "• small Promise was finished",
+      "• oh! I am within end(",
+      "• I small promise ",
+      "• small Promise was finished"
+    ], 'check steps execution order');
+    done();
+  });
+});
+
+it('Should stop test execution after fail in within', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    let lines = stdout.match(/\S.+/g);
+
+    let errorInWithinList = getlines(lines, 'Check within with generator. Should stop test execution after fail in within', 'Check within with generator. Should stop test execution after fail in main block');
+    testStatus = errorInWithinList.pop();
+    testStatus.should.include('FAILED');
+    errorInWithinList.should.eql([
+      "• I small yield ",
+      "I am small yield string",
+      "Within blabla:",
+      "• Hey! I am within Begin. I get blabla",
+      "• Within blabla: I error step ",
+      "• oh! I am within end("
+    ], 'check steps execution order');
+
+    done();
+  });
+});
+
+it('Should stop test execution after fail in main block', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    let lines = stdout.match(/\S.+/g);
+
+    let errorInTestList = getlines(lines, 'Check within with generator. Should stop test execution after fail in main block', '-- FAILURES:');
+    testStatus = errorInTestList.pop();
+    testStatus.should.include('FAILED');
+    errorInTestList.should.eql([
+      "• I error step ",
+      "• oh! I am within end("
+    ], 'check steps execution order');
+
+    done();
+  });
+});
+
+it('should return correct result after tests', (done) => {
+  exec(codecept_run+ ' --steps', (err, stdout, stderr) => {
+    stdout.should.include(' FAIL  | 4 passed, 2 failed');
+
+    done();
+  });
+});
+
+});