Repository: Codeception/CodeceptJS
Pull request #1514 (2019-02-21 12:53:46)

Add junit example in Multi Reports
I spend few hours before understand how use XUnit with Mocha Multi Reports.
Thanks God I found example of configuration in https://github.com/Codeception/CodeceptJS/issues/1502



Patch:
diff --git a/docs/locators.md b/docs/locators.md
index 9704f6b5..c1e9c7b8 100644
--- a/docs/locators.md
+++ b/docs/locators.md
@@ -137,6 +137,15 @@ Finds an element which contains a child element provided:
 locate('form').withChild('select');
 ```
 
+#### withDescendant
+
+Finds an element which contains a descendant element provided:
+
+```js
+// finds form with <select> which is the descendant it
+locate('form').withDescendant('select');
+```
+
 #### withText
 
 Finds element containing a text
diff --git a/docs/reports.md b/docs/reports.md
index d3ef8b09..b7395f57 100644
--- a/docs/reports.md
+++ b/docs/reports.md
@@ -286,6 +286,12 @@ Configure mocha-multi with reports that you want:
        "options": {
          "reportDir": "./output",
          "reportFilename": "report"
+      },
+      "mocha-junit-reporter": {
+        "stdout": "./output/console.log",
+        "options": {
+          "mochaFile": "./output/result.xml"
+        }
       }
     }
   }
diff --git a/docs/translation.md b/docs/translation.md
index 61a2df57..df250129 100644
--- a/docs/translation.md
+++ b/docs/translation.md
@@ -146,3 +146,43 @@ Scenario('成功提交表單', (我) => {
     我.在當前網址中看不到('/documentation')
 });
 ```
+
+## Japanese
+
+Add to config
+
+```json
+  "translation": "ja-JP"
+```
+
+Now you can write test like this:
+
+```js
+Scenario('ログインできる', (私は) => {
+    私は.ページを移動する('/login');
+    私は.フィールドに入力する("Eメール", "foo@example.com");
+    私は.フィールドに入力する("パスワード", "p@ssword");
+    私は.クリックする('ログイン');
+    私は.待つ(1);
+    私は.URLに含まれるか確認する('/home');
+});
+```
+
+## Using your own translation file
+
+Create translation file like this:
+
+```js
+module.exports = {
+  I: '',
+  actions: {
+    click: 'Klicken',
+    wait: 'Wartenn',
+  }
+```
+
+And add the file path to your config
+
+```json
+    "translation": "./path/to/your/translation.js"
+```
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 081570fa..8a7e8405 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -99,6 +99,45 @@ const webRoot = 'body';
  * }
  * ```
  *
+ * ### Internet Explorer
+ * Additional configuration params can be used from [IE options](https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/IE/Options.html)
+ *
+ * ```json
+ * {
+ *    "helpers": {
+ *      "WebDriver" : {
+ *        "url": "http://localhost",
+ *        "browser": "internet explorer",
+ *        "desiredCapabilities": {
+ *          "ieOptions": {
+ *            "ie.browserCommandLineSwitches": "-private",
+ *            "ie.usePerProcessProxy": true,
+ *            "ie.ensureCleanSession": true,
+ *          }
+ *        }
+ *      }
+ *    }
+ * }
+ * ```
+ *
+ * ### Selenoid Options
+ *
+ * ```json
+ * {
+ *    "helpers": {
+ *      "WebDriver" : {
+ *        "url": "http://localhost",
+ *        "browser": "chrome",
+ *        "desiredCapabilities": {
+ *          "selenoidOptions": {
+ *            "enableVNC": true,
+ *          }
+ *        }
+ *      }
+ *    }
+ * }
+ * ```
+ *
  * ### Connect through proxy
  *
  * CodeceptJS also provides flexible options when you want to execute tests to Selenium servers through proxy. You will
@@ -284,6 +323,14 @@ class WebDriver extends Helper {
       config.capabilities['moz:firefoxOptions'] = config.capabilities.firefoxOptions;
       delete config.capabilities.firefoxOptions;
     }
+    if (config.capabilities.ieOptions) {
+      config.capabilities['se:ieOptions'] = config.capabilities.ieOptions;
+      delete config.capabilities.ieOptions;
+    }
+    if (config.capabilities.selenoidOptions) {
+      config.capabilities['selenoid:options'] = config.capabilities.selenoidOptions;
+      delete config.capabilities.selenoidOptions;
+    }
 
     config.waitForTimeout /= 1000; // convert to seconds
 
@@ -1244,7 +1291,7 @@ class WebDriver extends Helper {
       const elem = usingFirstElement(res);
       const elementId = getElementId(elem);
       if (this.browser.isMobile) return this.browser.touchScroll(elementId, offsetX, offsetY);
-      const location = await this.browser.getElementLocation(elementId);
+      const location = await elem.getLocation();
       assertElementExists(location, 'Failed to receive', 'location');
       /* eslint-disable prefer-arrow-callback */
       return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, location.x + offsetX, location.y + offsetY);
@@ -1286,6 +1333,8 @@ class WebDriver extends Helper {
     }
 
     /* eslint-disable prefer-arrow-callback, comma-dangle, prefer-const */
+    const originalWindowSize = await this.browser.getWindowSize();
+
     let { width, height } = await this.browser.execute(function () {
       return {
         height: document.body.scrollHeight,
@@ -1298,7 +1347,9 @@ class WebDriver extends Helper {
 
     await this.browser.setWindowSize(width, height);
     this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`);
-    return this.browser.saveScreenshot(outputFile);
+    const buffer = await this.browser.saveScreenshot(outputFile);
+    await this.browser.setWindowSize(originalWindowSize.width, originalWindowSize.height);
+    return buffer;
   }
 
 
diff --git a/lib/locator.js b/lib/locator.js
index afdf2fea..8629feaa 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -116,7 +116,12 @@ class Locator {
   }
 
   withChild(locator) {
-    const xpath = sprintf('%s[//%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s//child::%s', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    return new Locator({ xpath });
+  }
+
+  withDescendant(locator) {
+    const xpath = sprintf('%s//descendant::%s', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
     return new Locator({ xpath });
   }
 
diff --git a/lib/plugin/puppeteerCoverage.js b/lib/plugin/puppeteerCoverage.js
new file mode 100644
index 00000000..d0126a32
--- /dev/null
+++ b/lib/plugin/puppeteerCoverage.js
@@ -0,0 +1,155 @@
+const Container = require('../container');
+const recorder = require('../recorder');
+const event = require('../event');
+const output = require('../output');
+const fs = require('fs');
+const path = require('path');
+const { clearString } = require('../utils');
+const debug = require('debug')('codeceptjs:plugin:puppeteerCoverage');
+
+const defaultConfig = {
+  coverageDir: 'output/coverage',
+  uniqueFileName: true,
+};
+
+const supportedHelpers = ['Puppeteer'];
+
+/**
+ * Builds the filename based on the test title
+ */
+function buildFileName(test, uniqueFileName) {
+  let fileName = clearString(test.title);
+
+  // This prevent data driven to be included in the failed screenshot file name
+  if (fileName.indexOf('{') !== -1) {
+    fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim();
+  }
+
+  if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') {
+    fileName = clearString(`${test.title}_${test.ctx.test.title}`);
+  }
+
+  if (uniqueFileName) {
+    const uuid =
+      test.uuid ||
+      test.ctx.test.uuid ||
+      Math.floor(new Date().getTime() / 1000);
+    fileName = `${fileName.substring(0, 10)}_${uuid}.coverage.json`;
+  } else {
+    fileName = `${fileName}.coverage.json`;
+  }
+
+  return fileName;
+}
+
+/**
+ * Dumps puppeteers code coverage after every test.
+ *
+ * ##### Configuration
+ *
+ * Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).
+ *
+ * ```js
+ * "plugins": {
+ *    "puppeteerCoverage": {
+ *      "enabled": true
+ *    }
+ * }
+ * ```
+ *
+ * Possible config options:
+ *
+ * * `outputDir`: directory to dump coverage files
+ * * `uniqueFileName`: generate a unique filename by adding uuid
+ *
+ * Notes:
+ *  First of all, YMMV!
+ *  To work, you need the client javascript code to be NOT uglified.  They need to be built in "development" mode.
+ *  And the end of your tests, you'll get a directory full of coverage per test run.  Now what?
+ *  You'll need to convert the coverage code to something istanbul can read.  Good news is someone wrote the code
+ *  for you (see puppeteer-to-istanbul link below).  Then using istanbul you need to combine the converted
+ *  coverage and create a report.  Good luck!
+ *
+ *  Links:
+ *  * https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
+ *  * https://github.com/istanbuljs/puppeteer-to-istanbul
+ *  * https://github.com/gotwarlost/istanbul
+ */
+module.exports = function (config) {
+  const helpers = Container.helpers();
+  let coverageRunning = false;
+  let helper;
+
+  for (const helperName of supportedHelpers) {
+    if (Object.keys(helpers).indexOf(helperName) > -1) {
+      helper = helpers[helperName];
+    }
+  }
+
+  if (!helper) {
+    console.error('Coverage is only supported in Puppeteer');
+    return; // no helpers for screenshot
+  }
+
+  const options = Object.assign(defaultConfig, helper.options, config);
+
+  event.dispatcher.on(event.all.before, async (suite) => {
+    output.debug('*** Collecting coverage for tests ****');
+  });
+
+  /**
+   *  Hack!  we're going to try to "start" coverage before each step because this is
+   *  when the browser is already up and is ready to start coverage.
+   */
+  event.dispatcher.on(event.step.before, async (step) => {
+    recorder.add(
+      'starting coverage',
+      async () => {
+        try {
+          if (!coverageRunning) {
+            debug('--> starting coverage <--');
+            coverageRunning = true;
+            await helper.page.coverage.startJSCoverage();
+          }
+        } catch (err) {
+          console.error(err);
+        }
+      },
+      true,
+    );
+  });
+
+  /**
+   * Save puppeteer coverage data after every test run
+   */
+  event.dispatcher.on(event.test.after, async (test) => {
+    recorder.add(
+      'saving coverage',
+      async () => {
+        try {
+          if (coverageRunning) {
+            debug('--> stopping coverage <--');
+            coverageRunning = false;
+            const coverage = await helper.page.coverage.stopJSCoverage();
+
+            const coverageDir = path.resolve(
+              process.cwd(),
+              options.coverageDir,
+            );
+            fs.mkdirSync(coverageDir, { recursive: true });
+
+            const coveragePath = path.resolve(
+              coverageDir,
+              buildFileName(test, options.uniqueFileName),
+            );
+            output.print(`writing ${coveragePath}`);
+            fs.writeFileSync(coveragePath, JSON.stringify(coverage));
+          }
+        } catch (err) {
+          console.error(err);
+        }
+      },
+      true,
+    );
+  });
+};
diff --git a/translations/index.js b/translations/index.js
index 405658d6..f5285e69 100644
--- a/translations/index.js
+++ b/translations/index.js
@@ -4,3 +4,4 @@ exports['it-IT'] = require('./it-IT');
 exports['pl-PL'] = require('./pl-PL');
 exports['zh-CN'] = require('./zh-CN');
 exports['zh-TW'] = require('./zh-TW');
+exports['ja-JP'] = require('./ja-JP');
diff --git a/translations/ja-JP.js b/translations/ja-JP.js
new file mode 100644
index 00000000..c8fd1005
--- /dev/null
+++ b/translations/ja-JP.js
@@ -0,0 +1,55 @@
+module.exports = {
+  I: '私は',
+  actions: {
+    amOutsideAngularApp: 'Angularの外に出る',
+    amInsideAngularApp: 'Angularの中に入る',
+    waitForElement: '要素を待つ',
+    waitForClickable: 'クリック可能になるまで待つ',
+    waitForVisible: '要素が見えるようになるまで待つ',
+    waitForText: 'テキストが表示されるまで待つ',
+    moveTo: '移動する',
+    refresh: '更新する',
+    haveModule: 'モジュールを持っている',
+    resetModule: 'モジュールをリセットする',
+    amOnPage: 'ページを移動する',
+    click: 'クリックする',
+    doubleClick: 'ダブルクリックする',
+    see: 'テキストがあるか確認する',
+    dontSee: 'テキストがないことを確認する',
+    selectOption: 'オプションを選ぶ',
+    fillField: 'フィールドに入力する',
+    pressKey: 'キー入力する',
+    triggerMouseEvent: 'マウスイベントを発火させる',
+    attachFile: 'ファイルを添付する',
+    seeInField: 'フィールドに文字が入っているか確認する',
+    dontSeeInField: 'フィールドに文字が入っていないか確認する',
+    appendField: 'フィールドに文字を追加する',
+    checkOption: 'オプションをチェックする',
+    seeCheckboxIsChecked: 'チェックされているか確認する',
+    dontSeeCheckboxIsChecked: 'チェックされていないことを確認する',
+    grabTextFrom: 'テキストを取得する',
+    grabValueFrom: '入力値を取得する',
+    grabAttributeFrom: '要素を取得する',
+    seeInTitle: 'タイトルに文字が含まれるか確認する',
+    dontSeeInTitle: 'タイトルに文字が含まれないことを確認する',
+    grabTitle: 'タイトルを取得する',
+    seeElement: '要素があるか確認する',
+    dontSeeElement: '要素がないことを確認する',
+    seeInSource: 'ソースにあるか確認する',
+    dontSeeInSource: 'ソースにないことを確認する',
+    executeScript: 'スクリプトを実行する',
+    executeAsyncScript: '非同期スクリプトを実行する',
+    seeInCurrentUrl: 'URLに含まれるか確認する',
+    dontSeeInCurrentUrl: 'URLに含まれないことを確認する',
+    seeCurrentUrlEquals: 'URLが等しいか確認する',
+    dontSeeCurrentUrlEquals: 'URLが等しくないことを確認する',
+    saveScreenshot: 'スクリーンショットを保存する',
+    setCookie: 'Cookieをセットする',
+    clearCookie: 'Cookieをクリアする',
+    seeCookie: 'Cookieに含まれることを確認する',
+    dontSeeCookie: 'Cookieに含まれないことを確認する',
+    grabCookie: 'Cookieを取得する',
+    resizeWindow: 'ウィンドウをリサイズする',
+    wait: '待つ',
+  },
+};


Test patch:
diff --git a/test/unit/locator_test.js b/test/unit/locator_test.js
index b09fdeda..567abe88 100644
--- a/test/unit/locator_test.js
+++ b/test/unit/locator_test.js
@@ -44,6 +44,12 @@ const xml = `<body>
         <label for="remember" class="optional">Remember Me</label>
       </div>
     </div>
+    <div class="form-field">
+      <input name="name0" label="Выберите услугу" type="text" value=""/>
+    </div>
+    <div class="form-field">
+      <input name="name1" label="Выберите услугу" type="text" value=""/>
+    </div>
   </fieldset>
   <label>Hello<a href="#">Please click</a></label>
   </div>
@@ -106,6 +112,15 @@ describe('Locator', () => {
     expect(nodes[0].firstChild.data).to.eql('Please click', l.toXPath());
   });
 
+
+  it('should select child element by name', () => {
+    const l = Locator.build('.form-field')
+      .withDescendant(Locator.build('//input[@name="name1"]'));
+    const nodes = xpath.select(l.toXPath(), doc);
+
+    expect(nodes).to.have.length(1, l.toXPath());
+  });
+
   it('should select element by siblings', () => {
     const l = Locator.build('//table')
       .withChild('td')