Repository: Codeception/CodeceptJS
Pull request #956 (2018-02-20 11:43:11)

[Puppeteer] Fixing function calls when clearing the cookies and localstorage
#### Highlights

Fixing bug where a non-existant functions were being called when trying to clear the cookies and localstorage. Introduced with the Puppeteer restart = false browser logic. #954 

elena-dev-thought: Hi! Do you have an ideea when the fix will be merged into the master? 
We are depending on this feature for the fast local testing enabling. 
I now that I can fork this branch, but I don't want to risk the downtime when this branch will be merged and deleted 🍡 .

Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index 630e1ba5..fc720e62 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -25,6 +25,7 @@ program.command('list [path]')
 
 program.command('def [path]')
   .description('List all actions for I.')
+  .option('-c, --config [file]', 'configuration file to be used')
   .action(require('../lib/command/definitions'));
 
 program.command('generate:test [path]')
diff --git a/docs/commands.md b/docs/commands.md
index 8f6ecf75..67b17d03 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -131,6 +131,7 @@ TypeScript Definitions allows IDEs to provide autocompletion when writing tests.
 
 ```sh
 codeceptjs def
+codeceptjs def --config path/to/codecept.json
 ```
 
 Produces `steps.d.ts` file, which referenced in the very beginning of a test file.
diff --git a/docs/webapi/grabCurrentUrl.mustache b/docs/webapi/grabCurrentUrl.mustache
new file mode 100644
index 00000000..d8092bd5
--- /dev/null
+++ b/docs/webapi/grabCurrentUrl.mustache
@@ -0,0 +1,7 @@
+Get current URL from browser.
+Resumes test execution, so should be used inside an async function.
+
+```js
+let url = await I.grabCurrentUrl();
+console.log(`Current URL is [${url}]`);
+```
\ No newline at end of file
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index a0532f21..346788b8 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -29,9 +29,10 @@ declare module "codeceptjs" {
 }
 `;
 
-module.exports = function (genPath) {
-  const testsPath = getTestRoot(genPath);
-  const config = getConfig(testsPath);
+module.exports = function (genPath, options) {
+  const configFile = options.config || genPath;
+  const testsPath = getTestRoot(configFile);
+  const config = getConfig(configFile);
   if (!config) return;
 
   const codecept = new Codecept(config, {});
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index d69d8921..10a94b3a 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -378,6 +378,13 @@ class Nightmare extends Helper {
     return this.browser.title();
   }
 
+  /**
+   * {{> ../webapi/grabCurrentUrl }}
+   */
+  async grabCurrentUrl() {
+    return this.browser.url();
+  }
+
   /**
    * {{> ../webapi/seeInCurrentUrl }}
    */
@@ -430,7 +437,7 @@ class Nightmare extends Helper {
   async seeElement(locator) {
     locator = new Locator(locator, 'css');
     const num = await this.browser.evaluate((by, locator) => {
-      return window.codeceptjs.findElements(by, locator).filter(e => e.offsetParent !== null).length;
+      return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
     }, locator.type, locator.value);
     equals('number of elements on a page').negate(0, num);
   }
@@ -442,7 +449,7 @@ class Nightmare extends Helper {
     locator = new Locator(locator, 'css');
     locator = new Locator(locator, 'css');
     const num = await this.browser.evaluate((by, locator) => {
-      return window.codeceptjs.findElements(by, locator).filter(e => e.offsetParent !== null).length;
+      return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
     }, locator.type, locator.value);
     equals('number of elements on a page').assert(0, num);
   }
@@ -843,7 +850,7 @@ class Nightmare extends Helper {
     return this.browser.wait((by, locator) => {
       const el = window.codeceptjs.findElement(by, locator);
       if (!el) return false;
-      return el.offsetParent !== null;
+      return el.offsetWidth > 0 && el.offsetHeight > 0;
     }, locator.type, locator.value).catch((err) => {
       if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
         throw new Error(`element (${JSON.stringify(locator)}) still not visible on page after ${sec} sec`);
@@ -861,7 +868,7 @@ class Nightmare extends Helper {
     return this.browser.wait((by, locator) => {
       const el = window.codeceptjs.findElement(by, locator);
       if (!el) return false;
-      return el.offsetParent === null;
+      return !(el.offsetWidth > 0 && el.offsetHeight > 0);
     }, locator.type, locator.value).catch((err) => {
       if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
         throw new Error(`element (${JSON.stringify(locator)}) still visible after ${sec} sec`);
diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js
index 03ad21d4..8b8ad7e7 100644
--- a/lib/helper/Protractor.js
+++ b/lib/helper/Protractor.js
@@ -390,6 +390,13 @@ class Protractor extends Helper {
     return proceedSee.call(this, 'negate', text, context);
   }
 
+  /**
+   * {{> ../webapi/grabCurrentUrl }}
+   */
+  async grabCurrentUrl() {
+    return this.browser.getCurrentUrl();
+  }
+
   /**
    * {{> ../webapi/selectOption }}
    */
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index f53e9303..33344d54 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -55,6 +55,18 @@ const consoleLogStore = new Console();
  *   "executablePath" : "/path/to/Chrome"
  * }
  * ```
+ *
+ * ## Access From Helpers
+ *
+ * Receive Puppeteer client from a custom helper by accessing `browser` for the Browser object or `page` for the current Page object:
+ *
+ * ```js
+ * const browser = this.helpers['Puppeteer'].browser;
+ * await browser.pages(); // List of pages in the browser
+ *
+ * const currentPage = this.helpers['Puppeteer'].page;
+ * await currentPage.url(); // Get the url of the current page
+ * ```
  */
 class Puppeteer extends Helper {
   constructor(config) {
@@ -130,9 +142,9 @@ class Puppeteer extends Helper {
 
     if (!this.options.keepCookies) {
       this.debugSection('Session', 'cleaning cookies and localStorage');
-      await this.browser.deleteCookie();
+      await this.clearCookie();
     }
-    await this.browser.execute('localStorage.clear();').catch((err) => {
+    await this.executeScript('localStorage.clear();').catch((err) => {
       if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
     });
     await this.closeOtherTabs();
@@ -938,6 +950,13 @@ class Puppeteer extends Helper {
     return logs;
   }
 
+  /**
+   * {{> ../webapi/grabCurrentUrl }}
+   */
+  async grabCurrentUrl() {
+    return this.page.url();
+  }
+
   /**
    * {{> ../webapi/seeInSource }}
    */
@@ -1234,6 +1253,97 @@ class Puppeteer extends Helper {
     }));
   }
 
+  /**
+   * {{> ../webapi/waitForEnabled }}
+   */
+  async waitForEnabled(locator, sec) {
+    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
+    locator = new Locator(locator, 'css');
+    const matcher = await this.context;
+    let waiter;
+    const context = await this._getContext();
+    if (locator.isCSS()) {
+      const enabledFn = function (locator) {
+        const els = document.querySelectorAll(locator);
+        if (!els || els.length === 0) {
+          return false;
+        }
+        return Array.prototype.filter.call(els, el => !el.disabled).length > 0;
+      };
+      waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value);
+    } else {
+      const enabledFn = function (locator, $XPath) {
+        eval($XPath); // eslint-disable-line no-eval
+        return $XPath(null, locator).filter(el => !el.disabled).length > 0;
+      };
+      waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
+    }
+    return waiter.catch((err) => {
+      throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`);
+    });
+  }
+
+  /**
+   * {{> ../webapi/waitForValue }}
+   */
+  async waitForValue(locator, value, sec) {
+    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
+    locator = new Locator(locator, 'css');
+    const matcher = await this.context;
+    let waiter;
+    const context = await this._getContext();
+    if (locator.isCSS()) {
+      const valueFn = function (locator, value) {
+        const els = document.querySelectorAll(locator);
+        if (!els || els.length === 0) {
+          return false;
+        }
+        return Array.prototype.filter.call(els, el => (el.value || '').indexOf(value) !== -1).length > 0;
+      };
+      waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value);
+    } else {
+      const valueFn = function (locator, $XPath, value) {
+        eval($XPath); // eslint-disable-line no-eval
+        return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
+      };
+      waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value);
+    }
+    return waiter.catch((err) => {
+      const loc = locator.toString();
+      throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`);
+    });
+  }
+
+  /**
+   * {{> ../webapi/waitNumberOfVisibleElements }}
+   */
+  async waitNumberOfVisibleElements(locator, num, sec) {
+    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
+    locator = new Locator(locator, 'css');
+    const matcher = await this.context;
+    let waiter;
+    const context = await this._getContext();
+    if (locator.isCSS()) {
+      const visibleFn = function (locator, num) {
+        const els = document.querySelectorAll(locator);
+        if (!els || els.length === 0) {
+          return false;
+        }
+        return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num;
+      };
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num);
+    } else {
+      const visibleFn = function (locator, $XPath, num) {
+        eval($XPath); // eslint-disable-line no-eval
+        return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num;
+      };
+      waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num);
+    }
+    return waiter.catch((err) => {
+      throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`);
+    });
+  }
+
   /**
    * {{> ../webapi/waitForElement }}
    */
@@ -1795,4 +1905,3 @@ function $XPath(element, selector) {
   }
   return res;
 }
-
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index e1a8b6c4..4beffc91 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -893,14 +893,15 @@ class WebDriverIO extends Helper {
   }
 
   /**
-   * Get current URL from browser.
-   *
-   * ```js
-   * let url = yield I.grabBrowserUrl();
-   * console.log(`Current URL is [${url}]`);
-   * ```
+   * {{> ../webapi/grabCurrentUrl }}
    */
+  async grabCurrentUrl() {
+    const res = await this.browser.url();
+    return res.value;
+  }
+
   async grabBrowserUrl() {
+    console.log('grabBrowserUrl deprecated. Use grabCurrentUrl instead');
     const res = await this.browser.url();
     return res.value;
   }
@@ -1012,7 +1013,6 @@ class WebDriverIO extends Helper {
    */
   async grabNumberOfVisibleElements(locator) {
     const res = await this._locate(locator);
-    assertElementExists(res, locator);
 
     let selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT));
     if (!Array.isArray(selected)) selected = [selected];
@@ -1564,7 +1564,7 @@ class WebDriverIO extends Helper {
 
       if (!Array.isArray(selected)) selected = [selected];
       return selected.length === num;
-    }, aSec * 1000, `The number of elements ${locator} is not ${num} after ${aSec} sec`);
+    }, aSec * 1000, `The number of elements (${locator}) is not ${num} after ${aSec} sec`);
   }
 
   /**


Test patch:
diff --git a/test/data/app/view/form/wait_enabled.php b/test/data/app/view/form/wait_enabled.php
new file mode 100644
index 00000000..e788d574
--- /dev/null
+++ b/test/data/app/view/form/wait_enabled.php
@@ -0,0 +1,21 @@
+<html>
+<body>
+
+<input id="text" type="text" name="test" disabled="true" value="some text">
+
+<button id="button" type="button" name="button1" disabled="true" value="first" onclick="updateMessage('button was clicked')">A Button</button>
+
+<div id="message"></div>
+
+<script>
+  setTimeout(function () {
+    document.querySelector('#text').disabled = false;
+    document.querySelector('#button').disabled = false;
+  }, 1000);
+
+  function updateMessage(msg) {
+    document.querySelector('#message').textContent = msg;
+  }
+</script>
+</body>
+</html>
diff --git a/test/data/app/view/form/wait_num_elements.php b/test/data/app/view/form/wait_num_elements.php
new file mode 100644
index 00000000..9c7e3c6a
--- /dev/null
+++ b/test/data/app/view/form/wait_num_elements.php
@@ -0,0 +1,26 @@
+<html>
+
+<body>
+
+<div id="context"></div>
+<script>
+
+var newDiv = document.createElement("div");
+newDiv.innerHTML = '<h1 class="title">Hello</h1>';
+
+var newDiv2 = document.createElement("div");
+newDiv2.innerHTML = '<h2 class="title">World</h2>';
+
+setTimeout(function () {
+    document.getElementById('context').appendChild(newDiv);
+}, 1000);
+
+setTimeout(function () {
+    document.getElementById('context').appendChild(newDiv2);
+}, 1500);
+
+
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/test/data/app/view/form/wait_value.php b/test/data/app/view/form/wait_value.php
new file mode 100644
index 00000000..314cdf4f
--- /dev/null
+++ b/test/data/app/view/form/wait_value.php
@@ -0,0 +1,17 @@
+<html>
+<body>
+
+<input id="text" class="inputbox" type="text" name="test" value="Grüße aus Hamburg">
+<input id="text2" class="inputbox" type="text" name="test2" value="Grüße aus London">
+
+<script>
+  setTimeout(function () {
+    document.querySelector('#text').value = 'Hello from Brisbane';
+  }, 2000);
+
+  setTimeout(function () {
+    document.querySelector('#text2').value = 'Hello from Brisbane';
+  }, 4000);
+</script>
+</body>
+</html>
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 12429c65..352c0eb5 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -89,6 +89,33 @@ describe('Puppeteer', function () {
     });
   });
 
+  describe('#waitNumberOfVisibleElements', () => {
+    it('should wait for a specified number of elements on the page', () => I.amOnPage('/info')
+      .then(() => I.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 3))
+      .then(() => I.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 2, 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
+      .catch((e) => {
+        e.message.should.include('The number of elements (//div[@id = "grab-multiple"]//a) is not 2 after 0.1 sec');
+      }));
+
+    it('should wait for a specified number of elements on the page using a css selector', () => I.amOnPage('/info')
+      .then(() => I.waitNumberOfVisibleElements('#grab-multiple > a', 3))
+      .then(() => I.waitNumberOfVisibleElements('#grab-multiple > a', 2, 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
+      .catch((e) => {
+        e.message.should.include('The number of elements (#grab-multiple > a) is not 2 after 0.1 sec');
+      }));
+
+    it('should wait for a specified number of elements which are not yet attached to the DOM', () => I.amOnPage('/form/wait_num_elements')
+      .then(() => I.waitNumberOfVisibleElements('.title', 2, 3))
+      .then(() => I.see('Hello'))
+      .then(() => I.see('World')));
+  });
+
   describe('#moveCursorTo', () => {
     it('should trigger hover event', () => I.amOnPage('/form/hover')
       .then(() => I.moveCursorTo('#hover'))
@@ -423,6 +450,55 @@ describe('Puppeteer', function () {
       }));
   });
 
+  describe('#waitForEnabled', () => {
+    it('should wait for input text field to be enabled', () => I.amOnPage('/form/wait_enabled')
+      .then(() => I.waitForEnabled('#text', 2))
+      .then(() => I.fillField('#text', 'hello world'))
+      .then(() => I.seeInField('#text', 'hello world')));
+
+    it('should wait for input text field to be enabled by xpath', () => I.amOnPage('/form/wait_enabled')
+      .then(() => I.waitForEnabled("//*[@name = 'test']", 2))
+      .then(() => I.fillField('#text', 'hello world'))
+      .then(() => I.seeInField('#text', 'hello world')));
+
+    it('should wait for a button to be enabled', () => I.amOnPage('/form/wait_enabled')
+      .then(() => I.waitForEnabled('#text', 2))
+      .then(() => I.click('#button'))
+      .then(() => I.see('button was clicked', '#message')));
+  });
+
+  describe('#waitForValue', () => {
+    it('should wait for expected value for given locator', () => I.amOnPage('/info')
+      .then(() => I.waitForValue('//input[@name= "rus"]', 'Верно'))
+      .then(() => I.waitForValue('//input[@name= "rus"]', 'Верно3', 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
+      .catch((e) => {
+        e.message.should.include('element (//input[@name= "rus"]) is not in DOM or there is no element(//input[@name= "rus"]) with value "Верно3" after 0.1 sec');
+      }));
+
+    it('should wait for expected value for given css locator', () => I.amOnPage('/form/wait_value')
+      .then(() => I.seeInField('#text', 'Hamburg'))
+      .then(() => I.waitForValue('#text', 'Brisbane', 2.5))
+      .then(() => I.seeInField('#text', 'Brisbane')));
+
+    it('should wait for expected value for given xpath locator', () => I.amOnPage('/form/wait_value')
+      .then(() => I.seeInField('#text', 'Hamburg'))
+      .then(() => I.waitForValue('//input[@value = "Grüße aus Hamburg"]', 'Brisbane', 2.5))
+      .then(() => I.seeInField('#text', 'Brisbane')));
+
+    it('should only wait for one of the matching elements to contain the value given xpath locator', () => I.amOnPage('/form/wait_value')
+      .then(() => I.waitForValue('//input[@type = "text"]', 'Brisbane', 4))
+      .then(() => I.seeInField('#text', 'Brisbane'))
+      .then(() => I.seeInField('#text2', 'London')));
+
+    it('should only wait for one of the matching elements to contain the value given css locator', () => I.amOnPage('/form/wait_value')
+      .then(() => I.waitForValue('.inputbox', 'Brisbane', 4))
+      .then(() => I.seeInField('#text', 'Brisbane'))
+      .then(() => I.seeInField('#text2', 'London')));
+  });
+
   describe('#seeCssPropertiesOnElements', () => {
     it('should check css property for given element', () => I.amOnPage('/info')
       .then(() => I.seeCssPropertiesOnElements('h3', {
diff --git a/test/helper/WebDriverIO_test.js b/test/helper/WebDriverIO_test.js
index d8d3d486..5029e7d7 100644
--- a/test/helper/WebDriverIO_test.js
+++ b/test/helper/WebDriverIO_test.js
@@ -210,6 +210,23 @@ describe('WebDriverIO', function () {
       }));
   });
 
+  describe('#waitForEnabled', () => {
+    it('should wait for input text field to be enabled', () => wd.amOnPage('/form/wait_enabled')
+      .then(() => wd.waitForEnabled('#text', 2))
+      .then(() => wd.fillField('#text', 'hello world'))
+      .then(() => wd.seeInField('#text', 'hello world')));
+
+    it('should wait for input text field to be enabled by xpath', () => wd.amOnPage('/form/wait_enabled')
+      .then(() => wd.waitForEnabled("//*[@name = 'test']", 2))
+      .then(() => wd.fillField('#text', 'hello world'))
+      .then(() => wd.seeInField('#text', 'hello world')));
+
+    it('should wait for a button to be enabled', () => wd.amOnPage('/form/wait_enabled')
+      .then(() => wd.waitForEnabled('#text', 2))
+      .then(() => wd.click('#button'))
+      .then(() => wd.see('button was clicked')));
+  });
+
   describe('#grabCssPropertyFrom', () => {
     it('should grab css property for given element', () => wd.amOnPage('/form/doubleclick')
       .then(() => wd.grabCssPropertyFrom('#block', 'height'))
@@ -302,18 +319,59 @@ describe('WebDriverIO', function () {
     it('should wait for expected value for given locator', () => wd.amOnPage('/info')
       .then(() => wd.waitForValue('//input[@name= "rus"]', 'Верно'))
       .then(() => wd.waitForValue('//input[@name= "rus"]', 'Верно3', 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
       .catch((e) => {
-        assert.equal(e.message, 'element (//input[@name= "rus"]) is not in DOM or there is no element(//input[@name= "rus"]) with value "Верно3" after 0.1 sec');
+        e.message.should.include('element (//input[@name= "rus"]) is not in DOM or there is no element(//input[@name= "rus"]) with value "Верно3" after 0.1 sec');
       }));
+
+    it('should wait for expected value for given css locator', () => wd.amOnPage('/form/wait_value')
+      .then(() => wd.seeInField('#text', 'Hamburg'))
+      .then(() => wd.waitForValue('#text', 'Brisbane', 2.5))
+      .then(() => wd.seeInField('#text', 'Brisbane')));
+
+    it('should wait for expected value for given xpath locator', () => wd.amOnPage('/form/wait_value')
+      .then(() => wd.seeInField('#text', 'Hamburg'))
+      .then(() => wd.waitForValue('//input[@value = "Grüße aus Hamburg"]', 'Brisbane', 2.5))
+      .then(() => wd.seeInField('#text', 'Brisbane')));
+
+    it('should only wait for one of the matching elements to contain the value given xpath locator', () => wd.amOnPage('/form/wait_value')
+      .then(() => wd.waitForValue('//input[@type = "text"]', 'Brisbane', 4))
+      .then(() => wd.seeInField('#text', 'Brisbane'))
+      .then(() => wd.seeInField('#text2', 'London')));
+
+    it('should only wait for one of the matching elements to contain the value given css locator', () => wd.amOnPage('/form/wait_value')
+      .then(() => wd.waitForValue('.inputbox', 'Brisbane', 4))
+      .then(() => wd.seeInField('#text', 'Brisbane'))
+      .then(() => wd.seeInField('#text2', 'London')));
   });
 
   describe('#waitNumberOfVisibleElements', () => {
     it('should wait for a specified number of elements on the page', () => wd.amOnPage('/info')
       .then(() => wd.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 3))
       .then(() => wd.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 2, 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
       .catch((e) => {
-        assert.equal(e.message, 'The number of elements //div[@id = "grab-multiple"]//a is not 2 after 0.1 sec');
+        e.message.should.include('The number of elements (//div[@id = "grab-multiple"]//a) is not 2 after 0.1 sec');
       }));
+
+    it('should wait for a specified number of elements on the page using a css selector', () => wd.amOnPage('/info')
+      .then(() => wd.waitNumberOfVisibleElements('#grab-multiple > a', 3))
+      .then(() => wd.waitNumberOfVisibleElements('#grab-multiple > a', 2, 0.1))
+      .then(() => {
+        throw Error('It should never get this far');
+      })
+      .catch((e) => {
+        e.message.should.include('The number of elements (#grab-multiple > a) is not 2 after 0.1 sec');
+      }));
+
+    it('should wait for a specified number of elements which are not yet attached to the DOM', () => wd.amOnPage('/form/wait_num_elements')
+      .then(() => wd.waitNumberOfVisibleElements('.title', 2, 3))
+      .then(() => wd.see('Hello'))
+      .then(() => wd.see('World')));
   });
 
   describe('#moveCursorTo', () => {
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index cb74b221..d1c5a469 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -23,7 +23,7 @@ module.exports.tests = function () {
     if (fileExists(dataFile)) require('fs').unlinkSync(dataFile);
   });
 
-  describe('current url : #seeInCurrentUrl, #seeCurrentUrlEquals, ...', () => {
+  describe('current url : #seeInCurrentUrl, #seeCurrentUrlEquals, #grabCurrentUrl, ...', () => {
     it('should check for url fragment', function* () {
       yield I.amOnPage('/form/checkbox');
       yield I.seeInCurrentUrl('/form');
@@ -41,6 +41,12 @@ module.exports.tests = function () {
       yield I.seeCurrentUrlEquals(`${siteUrl}/info`);
       return I.dontSeeCurrentUrlEquals(`${siteUrl}/form`);
     });
+
+    it('should grab browser url', function* () {
+      yield I.amOnPage('/info');
+      const url = yield I.grabCurrentUrl();
+      return assert.equal(url, `${siteUrl}/info`);
+    });
   });
 
   describe('see text : #see', () => {
diff --git a/test/runner/list_test.js b/test/runner/list_test.js
index 3d8aaa7c..46400193 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -30,4 +30,19 @@ describe('list/def commands', () => {
       done();
     });
   });
+
+  it('def should create definition file given a config file', (done) => {
+    try {
+      require('fs').unlinkSync(`${codecept_dir}/steps.d.ts`);
+    } catch (e) {
+      // continue regardless of error
+    }
+    exec(`${runner} def --config ${codecept_dir}/codecept.ddt.json`, (err, stdout, stderr) => {
+      stdout.should.include('Definitions were generated in steps.d.ts');
+      stdout.should.include('<reference path="./steps.d.ts" />');
+      require('fs').existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
+      assert(!err);
+      done();
+    });
+  });
 });