Repository: Codeception/CodeceptJS
Pull request #1663 (2019-05-21 21:54:06)

Updated gherkin interface in order to update table with placholder of exemple for scenario outlines
Hello,
With this PR when you create a gherkin file that combine a scenerio outline and Table: Example value will be replaced inside the table.
I added a new test : IncludeExamplesInDataTable.feature 

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


```diff

Issues
======
- Added 6
           

Clones added
============
- test/data/sandbox/features/step_definitions/my_steps.js  1
- lib/interfaces/gherkin.js  1
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=3617478&bid=12819780)
        
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Issues
======
- Added 1
           

Clones added
============
- test/data/sandbox/features/step_definitions/my_steps.js  1
- lib/interfaces/gherkin.js  1
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=3617478&bid=12819780)
        
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Clones added
============
- test/data/sandbox/features/step_definitions/my_steps.js  1
- lib/interfaces/gherkin.js  1
         
```


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

Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index 5586e171..63df5619 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -36,6 +36,7 @@ program.command('list [path]')
 program.command('def [path]')
   .description('Generates TypeScript definitions for all I actions.')
   .option('-c, --config [file]', 'configuration file to be used')
+  .option('-o, --output [folder]', 'target folder to paste definitions')
   .action(require('../lib/command/definitions'));
 
 program.command('gherkin:init [path]')
diff --git a/docs/bdd.md b/docs/bdd.md
index 729a62b9..2c9694ef 100644
--- a/docs/bdd.md
+++ b/docs/bdd.md
@@ -138,11 +138,11 @@ This scenarios are nice as live documentation but they do not test anything yet.
 Steps can be defined by executing `gherkin:snippets` command:
 
 ```bash
-codeceptjs gherkin:snippets
+codeceptjs gherkin:snippets [--path=PATH]
 ```
 
 This will produce code templates for all undefined steps in all feature files of this suite.
-It will also place stub definitions into `step_definitions/steps.js` file.
+It will also place stub definitions into `step_definitions/steps.js` file. However, you may also target a specific file to place all undefined steps in. This file must exist and be placed in the gherkin steps in the current config.
 Our next step will be to define those steps and transforming feature-file into a valid test.
 
 ### Step Definitions
diff --git a/docs/commands.md b/docs/commands.md
index c17cb226..471964f4 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -155,6 +155,13 @@ Feature('My new test');
 
 After doing that IDE should provide autocompletion for `I` object inside `Scenario` and `within` blocks.
 
+Add optional parameter `output` (or shortcat `-o`), if you want to place your definition file in specific folder:
+
+```sh
+codeceptjs def --output ./tests/typings
+codeceptjs def -o ./tests/typings
+```
+
 ## List Commands
 
 Prints all available methods of `I` to console
diff --git a/docs/examples.md b/docs/examples.md
index 1f2c3a9c..7dce2356 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -9,6 +9,13 @@ title: Examples
 CodeceptJS repo contains basic tests (both failing and passing) just to show how it works.
 Our team uses it to test new features and run simple scenarios.
 
+## [CodeceptJS Quick Start with Cucumber/Gherkin](https://github.com/gkushang/codeceptjs-quick-start)
+
+This repository demonstrate the CodeceptJS with Cucumber/Gherkin configuration
+
+* Easy 3-Steps process to colocate the CodeceptJS with Gherkin tests with your project
+* Quick Start with CodeceptJS wih Cucumber/Gherkin
+* Sample example and feature files to test GitHub Features
 
 ## [Testing Single Page Application](https://github.com/bugiratracker/codeceptjs-demo)
 
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 44160996..774dd7be 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -658,6 +658,29 @@ let url = await I.grabCurrentUrl();
 console.log(`Current URL is [${url}]`);
 ```
 
+### grabDataFromPerformanceTiming
+
+Grab the data from performance timing using Navigation Timing API.
+The returned data will contain following things in ms:
+
+-   responseEnd,
+-   domInteractive,
+-   domContentLoadedEventEnd,
+-   loadEventEnd
+    Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+await I.amOnPage('https://example.com');
+let data = await I.grabDataFromPerformanceTiming();
+//Returned data
+{ // all results are in [ms]
+responseEnd: 23,
+domInteractive: 44,
+domContentLoadedEventEnd: 196,
+loadEventEnd: 241
+}
+```
+
 ### grabHTMLFrom
 
 Retrieves the innerHTML from an element located by CSS or XPath and returns it to test.
diff --git a/docs/mobile-react-native-locators.md b/docs/mobile-react-native-locators.md
new file mode 100644
index 00000000..360284db
--- /dev/null
+++ b/docs/mobile-react-native-locators.md
@@ -0,0 +1,64 @@
+## Automating React Native apps
+
+### Problem
+Let's say we have a React Native app with component defined like this
+```html
+<Button testID='someButton'>My button</Button>
+```
+
+If you will try to execute a simple test like
+```js
+I.tap('~someButton')
+```
+it will work correctly on iOS (with XUITest driver), but on Android's UIAutomator2 it will give you an error
+```
+Touch actions like "tap" need at least some kind of position information like "element", "x" or "y" options, you've none given.
+```
+
+This happens because of the way React Native implements `testID` for Android, it puts provided value into the [View tag](https://developer.android.com/reference/android/view/View#tags),
+as could be found in the [source code](https://github.com/facebook/react-native/blob/19a88d7f4addcd9f95fd4908d50db37b3604b5b1/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java#L114).
+But UIAutomator doesn't have any means to work with view tags.
+
+### Solutions
+As many resources suggest (like [here](https://github.com/appium/appium/issues/6025#issuecomment-406141946) or [here](https://github.com/facebook/react-native/issues/7135)),
+you could use `testID` for iOS and `accesibilityLabel` for Android, but `accessibilityLabel` is a user-facing text that's intended for ... accesibility,
+not for UI tests, and you will need to switch it off for production builds.
+
+Another way to solve this issue is to use Espresso driver for Android.
+At first you need to enable Espresso driver for your Android configuration.
+You could do it just by changing `automationName` in the `helpers` section of the config file:
+```js
+{
+  //...
+  helpers: {
+    Appium: {
+      app: '/path/to/apk.apk',
+      platform: 'Android',
+      desiredCapabilities: {
+        automationName: 'Espresso',
+        platformVersion: '9',
+        deviceName: 'Android Emulator'
+      }
+    }
+  }
+  //...
+}
+```
+Then you could locate components using XPath expression:
+```js
+I.tap({android: '//*[@view-tag="someButton"]', ios: '~someButton'})
+```
+This way test would work for both platforms without any changes in code.
+To simplify things further you could write a helper function:
+```js
+function tid(id) {
+  return {
+    android: `//*[@view-tag="${id}"]`,
+    ios: '~' + id
+  }
+}
+```
+Now test will look more concise
+```js
+I.tap(tid('someButton'));
+```
diff --git a/docs/mobile.md b/docs/mobile.md
index 96d253d1..af2769ef 100644
--- a/docs/mobile.md
+++ b/docs/mobile.md
@@ -181,6 +181,7 @@ Accessibility id is recommended to use for locating element, as it rarely change
 * iOS uses [UIAccessibilityIdentification](https://developer.apple.com/documentation/uikit/uiaccessibilityidentification)
 * Android `accessibility id` matches the content-description
 * Web view uses `[aria-label]` attribute as accessibility id
+* ReactNative for Android has some caveats you could find more [here](mobile-react-native-locators.md)
 
 Add `~` prefix to search for element by its accessibility id:
 
diff --git a/docs/webapi/grabDataFromPerformanceTiming.mustache b/docs/webapi/grabDataFromPerformanceTiming.mustache
new file mode 100644
index 00000000..99111d92
--- /dev/null
+++ b/docs/webapi/grabDataFromPerformanceTiming.mustache
@@ -0,0 +1,19 @@
+Grab the data from performance timing using Navigation Timing API.
+The returned data will contain following things in ms:
+- responseEnd,
+- domInteractive,
+- domContentLoadedEventEnd,
+- loadEventEnd
+Resumes test execution, so **should be used inside an async function with `await`** operator.
+
+```js
+await I.amOnPage('https://example.com');
+let data = await I.grabDataFromPerformanceTiming();
+//Returned data
+{ // all results are in [ms]
+  responseEnd: 23,
+  domInteractive: 44,
+  domContentLoadedEventEnd: 196,
+  loadEventEnd: 241
+}
+```
\ No newline at end of file
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index ef68d1d7..60d707f8 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -9,23 +9,24 @@ const fs = require('fs');
 const path = require('path');
 
 const template = `
-type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}) => void;
+type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}, ...args: any) => void;
 
 declare class FeatureConfig {
-  retry(times:number): FeatureConfig
-  timeout(seconds:number): FeatureConfig
-  config(config:object): FeatureConfig
-  config(helperName:string, config:object): FeatureConfig
+  retry(times: number): FeatureConfig
+  timeout(seconds: number): FeatureConfig
+  config(config: object): FeatureConfig
+  config(helperName: string, config: object): FeatureConfig
 }
 
 declare class ScenarioConfig {
-  throws(err:any) : ScenarioConfig;
+  throws(err: any) : ScenarioConfig;
   fails() : ScenarioConfig;
-  retry(times:number): ScenarioConfig
-  timeout(timeout:number): ScenarioConfig
-  inject(inject:object): ScenarioConfig
-  config(config:object): ScenarioConfig
-  config(helperName:string, config:object): ScenarioConfig
+  retry(times: number): ScenarioConfig
+  timeout(timeout: number): ScenarioConfig
+  inject(inject: object): ScenarioConfig
+  config(config: object): ScenarioConfig
+  config(helperName: string, config: object): ScenarioConfig
+  injectDependencies(dependencies: { [key: string]: any }): ScenarioConfig
 }
 
 interface ILocator {
@@ -119,11 +120,13 @@ declare function BeforeSuite(callback: ICodeceptCallback): void;
 declare function After(callback: ICodeceptCallback): void;
 declare function AfterSuite(callback: ICodeceptCallback): void;
 
+declare function inject(): { [key: string]: any };
 declare function locate(selector: LocatorOrString): Locator;
 declare function within(selector: LocatorOrString, callback: Function): Promise<any>;
-declare function session(selector: string, callback: Function): Promise<any>;
-declare function session(selector: string, config: any, callback: Function): Promise<any>;
+declare function session(selector: LocatorOrString, callback: Function): Promise<any>;
+declare function session(selector: LocatorOrString, config: any, callback: Function): Promise<any>;
 declare function pause(): void;
+declare function secret(secret: any): string;
 
 declare const codeceptjs: any;
 
@@ -135,7 +138,7 @@ declare namespace CodeceptJS {
 }
 
 declare module "codeceptjs" {
-    export = CodeceptJS;
+  export = CodeceptJS;
 }
 `;
 
@@ -151,6 +154,8 @@ module.exports = function (genPath, options) {
   const config = getConfig(configFile);
   if (!config) return;
 
+  const targetFolderPath = options.output && getTestRoot(options.output) || testsPath;
+
   const codecept = new Codecept(config, {});
   codecept.init(testsPath);
 
@@ -180,16 +185,12 @@ module.exports = function (genPath, options) {
 
   let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
   definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
-  if (callbackParams.length > 0) {
-    definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${callbackParams.join(', ')}`);
-  } else {
-    definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', '');
-  }
+  definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${callbackParams.join(', ')}`);
   if (translations) {
     definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, translations.I);
   }
 
-  fs.writeFileSync(path.join(testsPath, 'steps.d.ts'), definitionsTemplate);
+  fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsTemplate);
   output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs');
   output.print('Definitions were generated in steps.d.ts');
   output.print('Load them by adding at the top of a test file:');
diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js
index 6b735c4b..1520c86e 100644
--- a/lib/command/gherkin/snippets.js
+++ b/lib/command/gherkin/snippets.js
@@ -34,6 +34,10 @@ module.exports = function (genPath, options) {
     output.error('No gherkin features defined in config. Exiting');
     process.exit(1);
   }
+  if (options.path && !config.gherkin.steps.includes(options.path)) {
+    output.error(`You must include ${options.path} to the gherkin steps in your config file`);
+    process.exit(1);
+  }
 
   const files = [];
   glob.sync(config.gherkin.features, { cwd: global.codecept_dir }).forEach((file) => {
@@ -81,7 +85,12 @@ module.exports = function (genPath, options) {
 
   files.forEach(file => parseFile(file));
 
-  let stepFile = config.gherkin.steps[0];
+  let stepFile = options.path || config.gherkin.steps[0];
+  if (!fs.existsSync(stepFile)) {
+    output.error('Please enter a valid step file path ', stepFile);
+    process.exit(1);
+  }
+
   if (!fsPath.isAbsolute(stepFile)) {
     stepFile = fsPath.join(global.codecept_dir, stepFile);
   }
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index c48ee81b..6f40b057 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -30,6 +30,7 @@ const fs = require('fs');
 
 
 let puppeteer;
+let perfTiming;
 const popupStore = new Popup();
 const consoleLogStore = new Console();
 
@@ -488,6 +489,17 @@ class Puppeteer extends Helper {
     this.context = await this.page.mainFrame().$('body');
   }
 
+  _extractDataFromPerformanceTiming(timing, ...dataNames) {
+    const navigationStart = timing.navigationStart;
+
+    const extractedData = {};
+    dataNames.forEach((name) => {
+      extractedData[name] = timing[name] - navigationStart;
+    });
+
+    return extractedData;
+  }
+
   /**
    * {{> ../webapi/amOnPage }}
    */
@@ -496,6 +508,17 @@ class Puppeteer extends Helper {
       url = this.options.url + url;
     }
     await this.page.goto(url, { waitUntil: this.options.waitForNavigation });
+
+    const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)));
+
+    perfTiming = this._extractDataFromPerformanceTiming(
+      performanceTiming,
+      'responseEnd',
+      'domInteractive',
+      'domContentLoadedEventEnd',
+      'loadEventEnd',
+    );
+
     return this._waitForAction();
   }
 
@@ -1887,6 +1910,13 @@ class Puppeteer extends Helper {
   async _waitForAction() {
     return this.wait(this.options.waitForAction / 1000);
   }
+
+  /**
+   * {{> ../webapi/grabDataFromPerformanceTiming }}
+   */
+  async grabDataFromPerformanceTiming() {
+    return perfTiming;
+  }
 }
 
 module.exports = Puppeteer;
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index f6bea0f7..97901e25 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -885,7 +885,7 @@ class WebDriver extends Helper {
     const elementId = getElementId(elem);
 
     const isSelected = await this.browser.isElementSelected(elementId);
-    if (isSelected.value) return Promise.resolve(true);
+    if (isSelected) return Promise.resolve(true);
     return this.browser[clickMethod](elementId);
   }
 
@@ -904,7 +904,7 @@ class WebDriver extends Helper {
     const elementId = getElementId(elem);
 
     const isSelected = await this.browser.isElementSelected(elementId);
-    if (!isSelected.value) return Promise.resolve(true);
+    if (!isSelected) return Promise.resolve(true);
     return this.browser[clickMethod](elementId);
   }
 
diff --git a/lib/interfaces/gherkin.js b/lib/interfaces/gherkin.js
index 53b7fcdd..2dfa6f90 100644
--- a/lib/interfaces/gherkin.js
+++ b/lib/interfaces/gherkin.js
@@ -73,7 +73,7 @@ module.exports = (text) => {
           }
           const tags = child.tags.map(t => t.name);
           const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
-          const test = new Test(title, async () => runSteps(exampleSteps));
+          const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
           test.tags = suite.tags.concat(tags);
           suite.addTest(scenario.test(test));
         }
@@ -99,3 +99,19 @@ function transformTable(table) {
   }
   return str;
 }
+function addExampleInTable(exampleSteps, placeholders) {
+  const steps = JSON.parse(JSON.stringify(exampleSteps));
+  for (const placeholder in placeholders) {
+    steps.map((step) => {
+      step = Object.assign({}, step);
+      if (step.argument && step.argument.type === 'DataTable') {
+        for (const id in step.argument.rows) {
+          const cells = step.argument.rows[id].cells;
+          cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])));
+        }
+      }
+      return step;
+    });
+  }
+  return steps;
+}
diff --git a/package.json b/package.json
index 8c07a8c7..1877d4c7 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
   "dependencies": {
     "allure-js-commons": "^1.3.2",
     "archiver": "^3.0.0",
-    "axios": "^0.18.0",
+    "axios": "^0.19.0",
     "chalk": "^1.1.3",
     "commander": "^2.20.0",
     "css-to-xpath": "^0.1.0",


Test patch:
diff --git a/test/data/sandbox/features/IncludeExamplesInDataTable.feature b/test/data/sandbox/features/IncludeExamplesInDataTable.feature
new file mode 100644
index 00000000..8b9338f3
--- /dev/null
+++ b/test/data/sandbox/features/IncludeExamplesInDataTable.feature
@@ -0,0 +1,17 @@
+Feature: Include Examples in dataTtable placeholder
+
+  @IncludeExamplesIndataTtable
+  Scenario Outline: order a product with discount
+    Given I have this product in my cart
+      | data     | value      |
+      | name     | <name>     |
+      | price    | <price>    |
+      | discount | <discount> |
+      | quantity | <quantity> |
+
+    Then I should see total price is "<total>" $
+
+    Examples:
+      | name         | price | discount | quantity | total |
+      | iPhone 5     | 10    | 1        | 2        | 19.0  |
+      | Nuclear Bomb | 20    | 2        | 1        | 18.0  |
diff --git a/test/data/sandbox/features/step_definitions/my_steps.js b/test/data/sandbox/features/step_definitions/my_steps.js
index c536c028..4d02a0ae 100644
--- a/test/data/sandbox/features/step_definitions/my_steps.js
+++ b/test/data/sandbox/features/step_definitions/my_steps.js
@@ -35,6 +35,18 @@ Given('I login', () => {
   I.login('user', secret('password'));
 });
 
+Given(/^I have this product in my cart$/, (table) => {
+  let str = '';
+  for (const id in table.rows) {
+    const cells = table.rows[id].cells;
+    str += cells.map(c => c.value).map(c => c.slice(0, 15).padEnd(15)).join(' | ');
+    str += '\n';
+  }
+  console.log(str);
+});
+
+Then(/^I should see total price is "([^"]*)" \$$/, (price) => {
+});
 
 Before((test) => {
   console.log(`-- before ${test.title} --`);
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index f8fa6332..b80e162d 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -2,6 +2,7 @@ const TestHelper = require('../support/TestHelper');
 const Puppeteer = require('../../lib/helper/Puppeteer');
 const puppeteer = require('puppeteer');
 const should = require('chai').should();
+const expect = require('chai').expect;
 const assert = require('assert');
 const path = require('path');
 const fs = require('fs');
@@ -73,6 +74,17 @@ describe('Puppeteer', function () {
     });
   });
 
+  describe('grabDataFromPerformanceTiming', () => {
+    it('should return data from performance timing', async () => {
+      await I.amOnPage('/');
+      const res = await I.grabDataFromPerformanceTiming();
+      expect(res).to.have.property('responseEnd');
+      expect(res).to.have.property('domInteractive');
+      expect(res).to.have.property('domContentLoadedEventEnd');
+      expect(res).to.have.property('loadEventEnd');
+    });
+  });
+
   webApiTests.tests();
 
   describe('#waitForFunction', () => {
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 65374b4a..44499abf 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -664,4 +664,20 @@ describe('WebDriver', function () {
       assert.notEqual(before, after);
     });
   });
+
+  describe('#uncheckOption', () => {
+    it('should uncheck option that is currently checked', async () => {
+      await wd.amOnPage('/info');
+      await wd.uncheckOption('interesting');
+      await wd.dontSeeCheckboxIsChecked('interesting');
+    });
+
+    it('should NOT uncheck option that is NOT currently checked', async () => {
+      await wd.amOnPage('/info');
+      await wd.uncheckOption('interesting');
+      // Unchecking again should not affect the current 'unchecked' status
+      await wd.uncheckOption('interesting');
+      await wd.dontSeeCheckboxIsChecked('interesting');
+    });
+  });
 });
diff --git a/test/runner/bdd_test.js b/test/runner/bdd_test.js
index f599a993..94552462 100644
--- a/test/runner/bdd_test.js
+++ b/test/runner/bdd_test.js
@@ -77,6 +77,17 @@ describe('BDD Gherkin', () => {
     });
   });
 
+  it('should run feature with table and examples files', (done) => {
+    exec(config_run_config('codecept.bdd.json') + ' --steps --grep "Include Examples in dataTtable placeholder"', (err, stdout, stderr) => { //eslint-disable-line
+      stdout.should.include('name            | Nuclear Bomb ');
+      stdout.should.include('price           | 20 ');
+      stdout.should.include('name            | iPhone 5 ');
+      stdout.should.include('price           | 10 ');
+      assert(!err);
+      done();
+    });
+  });
+
   it('should run feature with tables', (done) => {
     exec(config_run_config('codecept.bdd.json') + ' --steps --grep "Checkout products"', (err, stdout, stderr) => { //eslint-disable-line
       stdout.should.include('Given I have products in my cart');