Repository: Codeception/CodeceptJS
Pull request #856 (2018-01-04 13:28:24)

Implements uncheckOption in WebDriverIO
PR for this issue: https://github.com/Codeception/CodeceptJS/issues/853

Code is extremely similar to checkOption(). I included docs but not tests. 

codacy-bot: 
![Codacy](https://www.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Clones added
============
- lib/helper/WebDriverIO.js  2
         
```


See the complete overview on [Codacy](https://www.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1205794&bid=6177686)
        

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a426f7c5..2f6dcfbd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -110,6 +110,7 @@ mocha test/runner
 Please try to add corresponding testcase to runner or unit.
 
 ## Running tests in Dockerized environment
+
 Instead of manually running php, json_server and selenium for before tests you
 can use `docker-compose` to run those automatically.
 You can find `docker-compose.yml` file in `test` directory and run all commands
@@ -117,12 +118,14 @@ from this directory. Currently we provide following commands to run tests with
 respective dependencies:
 
 ### Run unit tests
-``` shell
+
+```sh
 docker-compose run --rm test-unit
 ```
 
 ### Run helper tests
-``` shell
+
+```sh
 docker-compose run --rm test-helpers
 
 # or pass path to helper test to run specific helper,
@@ -134,21 +137,25 @@ docker-compose run --rm test-helpers test/rest
 ```
 
 ### Run acceptance tests
+
 To that we provide two separate services respectively for WebDriverIO and
 Nightmare tests:
-``` shell
+
+```sh
 docker-compose run --rm test-acceptance.webdriverio
 docker-compose run --rm test-acceptance.nightmare
 ```
 
 ### Running against specific Node version
+
 By default dockerized tests are run against node 6.9.5, you can run it against
 specific version as long as there is Docker container available for such
 version. To do that you need to build codecept's Docker image prior to running
 tests and pass `NODE_VERSION` as build argument.
 
 For example to prepare `test-helpers` containers based on node 8.7.0:
-``` shell
+
+```sh
 docker-compose build --build-arg NODE_VERSION=8.7.0 test-helpers
 ```
 
diff --git a/docs/acceptance.md b/docs/acceptance.md
index c1374b3b..54089c15 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -256,9 +256,9 @@ within({frame: [".content", "#editor"]}, () => {
   I.see('Page');
 });
 ```
+
 ---
 
 ### done()
 
 CodeceptJS through helpers provides user friendly API to interact with a webpage. In this section we described using WebDriverIO helper which allows to control browser through Selenium WebDriver.
-
diff --git a/docs/api/helper.md b/docs/api/helper.md
index 3f62fe72..33fa4600 100644
--- a/docs/api/helper.md
+++ b/docs/api/helper.md
@@ -2,31 +2,39 @@
 
 **Parameters**
 
--   `config`  
+-   `config`
 
 ## _after
 
 Hook executed after each test
 
+## _afterStep
+
+Hook executed after each step
+
+**Parameters**
+
+-   `step`
+
 ## _afterSuite
 
 Hook executed after each suite
 
 **Parameters**
 
--   `suite`  
+-   `suite`
 
 ## _before
 
 Hook executed before each test.
 
-## _beforeSuite
+## _beforeStep
 
 Hook executed before each step
 
 **Parameters**
 
--   `step`  
+-   `step`
 
 ## _beforeSuite
 
@@ -34,15 +42,7 @@ Hook executed before each suite
 
 **Parameters**
 
--   `suite`  
-
-## _beforeSuite
-
-Hook executed after each step
-
-**Parameters**
-
--   `step`  
+-   `suite`
 
 ## _failed
 
@@ -50,7 +50,7 @@ Hook executed after each failed test
 
 **Parameters**
 
--   `test`  
+-   `test`
 
 ## _finishTest
 
@@ -58,7 +58,7 @@ Hook executed after all tests are executed
 
 **Parameters**
 
--   `suite`  
+-   `suite`
 
 ## _init
 
@@ -71,7 +71,7 @@ Executed in the very beginning of a test
 
 **Parameters**
 
--   `test`  
+-   `test`
 
 ## debug
 
@@ -79,7 +79,7 @@ Print debug message to console (outputs only in debug mode)
 
 **Parameters**
 
--   `msg`  
+-   `msg`
 
 ## helpers
 
diff --git a/docs/configuration.md b/docs/configuration.md
index f23d4e3d..27199476 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -7,7 +7,7 @@ After running `codeceptjs init` it should be saved in test root.
 Here is an overview of available options with their defaults:
 
 * **tests**: `"./*_test.js"` - pattern to locate tests
-* **include**: `{}` - actors and page objects to be registered in DI container and included in tests. Accepts objects and module `require ` paths
+* **include**: `{}` - actors and page objects to be registered in DI container and included in tests. Accepts objects and module `require` paths
 * **timeout**: `10000` - default tests timeout
 * **output**: `"./output"` - where to store failure screenshots, etc
 * **helpers**: `{}` - list of enabled helpers
diff --git a/docs/docker.md b/docs/docker.md
index 1a8b476d..02285935 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -80,6 +80,7 @@ docker build -t codeception/codeceptjs .
 ```
 
 ### Debugging
+
 To pass arguments to `codecept run` command inside docker image you can set
 `CODECEPT_ARGS` environment variable. For example to run your tests with debug
 and steps output:
diff --git a/docs/installation.md b/docs/installation.md
index 270da97a..1522eb60 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,6 +1,6 @@
 # Installation
 
-### Local
+## Local
 
 Use NPM install CodeceptJS:
 
@@ -38,7 +38,6 @@ To use it with Puppeteer install puppeteer package:
 npm install puppeteer --save-dev
 ```
 
-
 ## Meta Packages
 
 By default it doesn't install any backends like Webdriverio, Protractor, or Nightmare, so you need to install corresponding packages manually, or install one of the provided meta-packages:
diff --git a/docs/pageobjects.md b/docs/pageobjects.md
index 090b4de4..8464cac7 100644
--- a/docs/pageobjects.md
+++ b/docs/pageobjects.md
@@ -68,6 +68,7 @@ Scenario('login', (I, loginPage) => {
 ```
 
 Also you can use `async/await` inside PageObject:
+
 ```js
 'use strict';
 let I;
@@ -109,7 +110,6 @@ Scenario('login2', async (I, loginPage, basePage) => {
 });
 ```
 
-
 Also you can use generators inside a PageObject:
 
 ```js
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index 9be1895f..6d0ee529 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -5,8 +5,8 @@ It operates over Google Chrome directly without requireing additional tools like
 
 CodeceptJS uses Puppeteer to improve end to end testing experience.
 
-1. No need to learn the syntax of a new tool, all drivers in CodeceptJS share the same API.
-2. CodeceptJS can locate elements by XPath.
+1.  No need to learn the syntax of a new tool, all drivers in CodeceptJS share the same API.
+2.  CodeceptJS can locate elements by XPath.
 
 Take a look at a sample test:
 
@@ -33,7 +33,6 @@ npm install -g codeceptjs-puppeteer
 Or see [alternative installation options](http://codecept.io/installation/)
 If you already have CodeceptJS project, just install `puppeteer` package and enable it in config.
 
-
 And a basic project initialized
 
 ```sh
@@ -68,7 +67,7 @@ Sometimes test may run faster than application gets rendered. In this case it is
 
 CodeceptJS test should be created with `gt` command:
 
-```
+```sh
 codeceptjs gt
 ```
 
@@ -160,9 +159,10 @@ Puppeteer has a very [rich and flexible API](https://github.com/GoogleChrome/pup
 
 Start with creating an `MyPuppeteer` helper using `generate:helper` or `gh` command:
 
-```
+```sh
 codeceptjs gh
 ```
+
 Then inside a Helper you can access `Puppeteer` helper of CodeceptJS.
 Let's say you want to create `I.renderPageToPdf` action. In this case you need to call `pdf` method of `page` object
 
diff --git a/docs/webapi/uncheckOption.mustache b/docs/webapi/uncheckOption.mustache
new file mode 100644
index 00000000..58f1f033
--- /dev/null
+++ b/docs/webapi/uncheckOption.mustache
@@ -0,0 +1,12 @@
+Unselects a checkbox or radio button.
+Element is located by label or name or CSS or XPath.
+
+The second parameter is a context (CSS or XPath locator) to narrow the search.
+
+```js
+I.uncheckOption('#agree');
+I.uncheckOption('I Agree to Terms and Conditions');
+I.uncheckOption('agree', '//form');
+```
+@param field checkbox located by label | name | CSS | XPath | strict locator
+@param context (optional) element located by CSS | XPath | strict locator
diff --git a/lib/codecept.js b/lib/codecept.js
index 4b58591d..b5433f46 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -20,7 +20,7 @@ class Codecept {
   constructor(config, opts) {
     this.config = Config.create(config);
     this.opts = opts;
-    this.testFiles = [];
+    this.testFiles = new Array(0);
   }
 
   /**
diff --git a/lib/data/table.js b/lib/data/table.js
index 9b22873e..e72bd014 100644
--- a/lib/data/table.js
+++ b/lib/data/table.js
@@ -4,7 +4,7 @@
 class DataTable {
   constructor(array) {
     this.array = array;
-    this.rows = [];
+    this.rows = new Array(0);
   }
 
   add(array) {
diff --git a/lib/helper.js b/lib/helper.js
index 6b6f6b96..0eb45c70 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -51,63 +51,65 @@ class Helper {
    * Hook provides a test details
    * Executed in the very beginning of a test
    *
-   * @param test
+   * @param {*} test
    */
-  _test() {
+  _test(test) {
 
   }
 
   /**
    * Hook executed after each failed test
    *
-   * @param test
+   * @param {*} test
    */
-  _failed() {
+  _failed(test) {
 
   }
 
   /**
    * Hook executed before each step
    *
-   * @param step
+   * @param {*} step
    * @override
-   *
-   * _beforeStep()
    */
+  _beforeStep(step) {
+
+  }
 
   /**
    * Hook executed after each step
    *
-   * @param step
+   * @param {*} step
    * @override
-   *
-   * _afterStep()
    */
+  _afterStep(step) {
+
+  }
 
   /**
    * Hook executed before each suite
    *
-   * @param suite
+   * @param {*} suite
    */
-  _beforeSuite() {
+  _beforeSuite(suite) {
 
   }
 
   /**
    * Hook executed after each suite
    *
-   * @param suite
+   * @param {*} suite
    */
-  _afterSuite() {
+  _afterSuite(suite) {
 
   }
 
   /**
    * Hook executed after all tests are executed
    *
-   * @param suite
+   * @param {*} suite
    */
-  _finishTest() {
+  _finishTest(suite) {
 
   }
 
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index 60a4dc29..d5e3f906 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -516,7 +516,7 @@ class Puppeteer extends Helper {
    * {{> ../webapi/dontSeeCurrentUrlEquals }}
    */
   async dontSeeCurrentUrlEquals(url) {
-    const currentUrl = this.page.url();
+    const currentUrl = await this.page.url();
     urlEquals(this.options.url).negate(url, currentUrl);
   }
 
@@ -530,7 +530,7 @@ class Puppeteer extends Helper {
   /**
    * {{> ../webapi/dontSee }}
    */
-  dontSee(text, context = null) {
+  async dontSee(text, context = null) {
     return proceedSee.call(this, 'negate', text, context);
   }
 
@@ -706,7 +706,7 @@ class Puppeteer extends Helper {
       waiter = this.page.waitForFunction($XPath, { timeout: waitTimeout }, null, locator.value);
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not present on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -729,7 +729,7 @@ class Puppeteer extends Helper {
       waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not visible on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not visible on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -751,7 +751,7 @@ class Puppeteer extends Helper {
       waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still not hidden after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -760,7 +760,8 @@ class Puppeteer extends Helper {
    * {{> ../webapi/waitForText }}
    */
   async waitForText(text, sec = null, context = null) {
-    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
+    const aSec = sec || this.options.waitForTimeout;
+    const waitTimeout = aSec * 1000;
     if (this.withinLocator) this.debug('waitForVisible ignores `within` block');
     let waiter;
 
@@ -786,7 +787,7 @@ class Puppeteer extends Helper {
     }
 
     return waiter.catch((err) => {
-      throw new Error(`Text "${text}" was not found on page after ${sec} sec`);
+      throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
@@ -794,7 +795,8 @@ class Puppeteer extends Helper {
    * {{> ../webapi/waitUntil }}
    */
   async waitUntil(fn, sec = null) {
-    const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
+    const aSec = sec || this.options.waitForTimeout;
+    const waitTimeout = aSec * 1000;
     return this.page.waitForFunction(fn, { timeout: waitTimeout });
   }
 
@@ -820,7 +822,7 @@ class Puppeteer extends Helper {
       waiter = this.page.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
     }
     return waiter.catch((err) => {
-      throw new Error(`element (${locator.toString()}) still on page after ${sec} sec\n${err.message}`);
+      throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
     });
   }
 
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index 9c35b4eb..deea9479 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -82,7 +82,7 @@ class REST extends Helper {
    * ```
    *
    * @param {*} url
-   * @param object headers
+   * @param {object} headers
    */
   sendGetRequest(url, headers = {}) {
     request = unirest.get(this._url(url)).headers(headers);
@@ -98,7 +98,7 @@ class REST extends Helper {
    *
    * @param {*} url
    * @param {*} payload
-   * @param object headers
+   * @param {object} headers
    */
   sendPostRequest(url, payload = {}, headers = {}) {
     request = unirest.post(this._url(url)).headers(headers).send(payload);
@@ -112,9 +112,9 @@ class REST extends Helper {
    * I.sendPatchRequest('/api/users.json', { "email": "user@user.com" });
    * ```
    *
-   * @param string url
-   * @param object payload
-   * @param object headers
+   * @param {string} url
+   * @param {object} payload
+   * @param {object} headers
    */
   sendPatchRequest(url, payload = {}, headers = {}) {
     request = unirest.patch(this._url(url)).headers(headers).send(payload);
@@ -128,9 +128,9 @@ class REST extends Helper {
    * I.sendPutRequest('/api/users.json', { "email": "user@user.com" });
    * ```
    *
-   * @param string url
-   * @param object payload
-   * @param object headers
+   * @param {string} url
+   * @param {object} payload
+   * @param {object} headers
    */
   sendPutRequest(url, payload = {}, headers = {}) {
     request = unirest.put(this._url(url)).headers(headers).send(payload);
@@ -145,7 +145,7 @@ class REST extends Helper {
    * ```
    *
    * @param {*} url
-   * @param {*} payload
+   * @param {object} headers
    */
   sendDeleteRequest(url, headers = {}) {
     request = unirest.delete(this._url(url)).headers(headers);
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index 0fda8066..627f0127 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -649,6 +649,24 @@ class WebDriverIO extends Helper {
     return this.browser[clickMethod](elem.ELEMENT);
   }
 
+  /**
+   * {{> ../webapi/uncheckOption }}
+   * Appium: not tested
+   */
+  async uncheckOption(field, context = null) {
+    const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick';
+    const locateFn = prepareLocateFn.call(this, context);
+
+    const res = await findCheckable.call(this, field, locateFn);
+
+    assertElementExists(res, field, 'Checkable');
+    const elem = res.value[0];
+
+    const isSelected = await this.browser.elementIdSelected(elem.ELEMENT);
+    if (!isSelected.value) return Promise.resolve(true);
+    return this.browser[clickMethod](elem.ELEMENT);
+  }
+
   /**
    * {{> ../webapi/grabTextFrom }}
    * Appium: support
@@ -901,6 +919,19 @@ class WebDriverIO extends Helper {
     return this.browser.log('browser').then(res => res.value);
   }
 
+  /**
+   * Get current URL from browser.
+   *
+   * ```js
+   * let url = yield I.grabBrowserUrl();
+   * console.log(`Current URL is [${url}]`);
+   * ```
+   */
+  async grabBrowserUrl() {
+    const res = await this.browser.url();
+    return res.value;
+  }
+
   /**
    * {{> ../webapi/dontSeeInSource }}
    * Appium: support
diff --git a/lib/mocha_factory.js b/lib/mocha_factory.js
index efa5cf23..274704e2 100644
--- a/lib/mocha_factory.js
+++ b/lib/mocha_factory.js
@@ -34,10 +34,11 @@ class MochaFactory {
       });
     }
 
-    if (reporterOptions['codeceptjs-cli-reporter']) {
+    const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter');
+    if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
       Object.defineProperty(
         reporterOptions, 'codeceptjs/lib/reporter/cli',
-        Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter'),
+        attributes,
       );
       delete reporterOptions['codeceptjs-cli-reporter'];
     }
diff --git a/package.json b/package.json
index 53ea63a7..9fa189b3 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
   },
   "repository": "Codeception/codeceptjs",
   "scripts": {
+    "build": "tsc -p ./",
     "json-server": "./node_modules/json-server/bin/index.js test/data/rest/db.json -p 8010 --watch -m test/data/rest/headers.js",
     "lint": "eslint bin/ examples/ lib/ test/ translations/",
     "lint-fix": "eslint bin/ examples/ lib/ test/ translations/ --fix",
@@ -46,7 +47,6 @@
     "requireg": "^0.1.5"
   },
   "devDependencies": {
-    "@types/commander": "^2.12.2",
     "@types/inquirer": "^0.0.35",
     "@types/node": "^8.5.1",
     "chai": "^3.4.1",
diff --git a/translations/pt-BR.js b/translations/pt-BR.js
index 2074a079..8bff406f 100644
--- a/translations/pt-BR.js
+++ b/translations/pt-BR.js
@@ -25,6 +25,7 @@ module.exports = {
     dontSeeInField: 'naoVejoNoCampo',
     appendField: 'adicionaCampo',
     checkOption: 'marcoOpcao',
+    uncheckOption: 'desmarcoOpcao',
     seeCheckboxIsChecked: 'vejoCheckboxMarcado',
     dontSeeCheckboxIsChecked: 'naoVejoCheckboxMarcado',
     grabTextFrom: 'pegoTextoDe',


Test patch:
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index ccb34330..6f78fd1b 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -2,7 +2,7 @@ version: '3'
 services:
   test-unit:
     build: ..
-    entrypoint: node_modules/.bin/gulp
+    entrypoint: docker/entrypoint node_modules/.bin/mocha
     command: test
     working_dir: /codecept
     volumes:
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 99483946..b9dd9e47 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -55,11 +55,15 @@ describe('CodeceptJS Interface', () => {
         '[1] Starting recording promises',
         '[1] Queued | hook FileSystem._beforeSuite()',
         '[1] Queued | hook FileSystem._before()',
+        '[1] Queued | hook FileSystem._beforeStep()',
         '[1] Queued | amInPath: "."',
+        '[1] Queued | hook FileSystem._afterStep()',
         '[1] Queued | step passed',
         '[1] Queued | return result',
         '[1] Queued | say hello world',
+        '[1] Queued | hook FileSystem._beforeStep()',
         '[1] Queued | seeFile: "codecept.json"',
+        '[1] Queued | hook FileSystem._afterStep()',
         '[1] Queued | step passed',
         '[1] Queued | return result',
         '[1] Queued | fire test.passed',
diff --git a/test/unit/steps_test.js b/test/unit/steps_test.js
index c5a460c0..abadbe86 100644
--- a/test/unit/steps_test.js
+++ b/test/unit/steps_test.js
@@ -25,7 +25,7 @@ describe('Step', () => {
     step.humanizeArgs().should.eql('"word", 1');
 
     step.args = [['some', 'data'], 1];
-    step.humanizeArgs().should.eql('[some,data], 1');
+    step.humanizeArgs().should.eql('["some","data"], 1');
 
     step.args = [{ css: '.class' }];
     step.humanizeArgs().should.eql('{"css":".class"}');