Repository: Codeception/CodeceptJS
Pull request #966 (2018-02-26 12:35:08)

[Docs|Puppeteer] Added comment about how to access the browser and current page object from custom helper
#### Highlights
Added comment about how to access the browser and current page object from custom helper. #963 


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/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/Puppeteer.js b/lib/helper/Puppeteer.js
index f53e9303..cfc64b88 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) {
@@ -1234,6 +1246,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 +1898,3 @@ function $XPath(element, selector) {
   }
   return res;
 }
-
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index e1a8b6c4..bb122db9 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -1012,7 +1012,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 +1563,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/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();
+    });
+  });
 });