Repository: Codeception/CodeceptJS
Pull request #1953 (2019-10-14 08:55:09)

Aria tests
Optimization for searching via `aria-labelledby` attribute
Replaces #1941 


Patch:
diff --git a/examples/codecept.conf.js b/examples/codecept.conf.js
index 6443b875..c8c324d1 100644
--- a/examples/codecept.conf.js
+++ b/examples/codecept.conf.js
@@ -1,9 +1,3 @@
-const { useHeadlessWhen, useSharedCookies } = require('@codeceptjs/config-hooks');
-
-useHeadlessWhen(process.env.headless);
-useSharedCookies();
-useWindowSize(1600, 1000);
-
 exports.config = {
   output: './output',
   helpers: {
@@ -12,7 +6,7 @@ exports.config = {
       browser: 'chrome',
       restart: false,
       windowSize: '1600x1200',
-      show: false,
+      show: true,
     },
     REST: {},
   },
diff --git a/lib/config.js b/lib/config.js
index c333ed10..4ca0b712 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,8 +1,11 @@
 const fs = require('fs');
 const path = require('path');
-const isFile = require('./utils').isFile;
-const fileExists = require('./utils').fileExists;
-const deepMerge = require('./utils').deepMerge;
+const {
+  fileExists,
+  isFile,
+  deepMerge,
+  deepClone,
+} = require('./utils');
 
 const defaultConfig = {
   output: './_output',
@@ -33,7 +36,7 @@ class Config {
    * @param {*} newConfig
    */
   static create(newConfig) {
-    config = deepMerge(defaultConfig, newConfig);
+    config = deepMerge(deepClone(defaultConfig), newConfig);
     hooks.forEach(f => f(config));
     return config;
   }
@@ -104,7 +107,7 @@ class Config {
    */
   static reset() {
     hooks = [];
-    return config = defaultConfig;
+    return config = Object.assign({}, defaultConfig);
   }
 }
 
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index d2b57c64..4326fc26 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -2190,7 +2190,10 @@ async function findFields(locator) {
   if (!locator.isFuzzy()) return this._locate(locator, true);
 
   const literal = xpathLocator.literal(locator.value);
-  let els = await this._locate(Locator.field.byText(literal));
+  let els = await this._locate(Locator.field.labelEquals(literal));
+  if (els.length) return els;
+
+  els = await this._locate(Locator.field.labelContains(literal));
   if (els.length) return els;
 
   els = await this._locate(Locator.field.byName(literal));
diff --git a/lib/locator.js b/lib/locator.js
index c2cbc64b..b6109495 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -209,7 +209,7 @@ Locator.clickable = {
     `.//button[./@name = ${literal}]`,
     `.//*[@aria-label = ${literal}]`,
     `.//*[@title = ${literal}]`,
-    `.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
+    `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
   ]),
 
   self: literal => `./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
@@ -217,19 +217,19 @@ Locator.clickable = {
 
 Locator.field = {
   labelEquals: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][((./@name = ${literal}) or ./@id = //label[normalize-space(string(.)) = ${literal}]/@for or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][((./@name = ${literal}) or ./@id = //label[@for][normalize-space(string(.)) = ${literal}]/@for or ./@placeholder = ${literal})]`,
     `.//label[normalize-space(string(.)) = ${literal}]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
   ]),
   labelContains: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
     `.//*[@aria-label = ${literal}]`,
     `.//*[@title = ${literal}]`,
-    `.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
+    `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
   ]),
   byName: literal => `.//*[self::input | self::textarea | self::select][@name = ${literal}]`,
   byText: literal => xpathLocator.combine([
-    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
+    `.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
   ]),
 
@@ -237,7 +237,7 @@ Locator.field = {
 
 Locator.checkable = {
   byText: literal => xpathLocator.combine([
-    `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
+    `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//input[@type = 'radio' or @type = 'checkbox']`,
   ]),
 
diff --git a/package.json b/package.json
index 838d3d36..72f9310a 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
     "dev:graphql": "nodemon test/data/graphql/index.js"
   },
   "dependencies": {
+    "@codeceptjs/configure": "^0.3.0",
     "allure-js-commons": "^1.3.2",
     "axios": "^0.19.0",
     "chalk": "^1.1.3",
@@ -73,10 +74,10 @@
     "@types/inquirer": "^0.0.35",
     "@types/mocha": "^5.2.7",
     "@types/node": "^8.10.54",
-    "@wdio/sauce-service": "^5.12.5",
-    "@wdio/selenium-standalone-service": "^5.12.1",
-    "@wdio/utils": "^5.13.0",
-    "apollo-server-express": "^2.9.3",
+    "@wdio/sauce-service": "^5.13.2",
+    "@wdio/selenium-standalone-service": "^5.13.2",
+    "@wdio/utils": "^5.13.2",
+    "apollo-server-express": "^2.9.4",
     "chai": "^3.4.1",
     "chai-as-promised": "^5.2.0",
     "documentation": "^8.1.2",
@@ -86,7 +87,7 @@
     "eslint-plugin-mocha": "^5.3.0",
     "express": "^4.17.1",
     "faker": "^4.1.0",
-    "graphql": "^14.5.7",
+    "graphql": "^14.5.8",
     "husky": "^1.2.1",
     "json-server": "^0.10.1",
     "nightmare": "^3.0.2",
@@ -99,7 +100,7 @@
     "testcafe": "^1.5.0",
     "typescript": "^2.9.2",
     "wdio-docker-service": "^1.5.0",
-    "webdriverio": "^5.13.1",
+    "webdriverio": "^5.13.2",
     "xmldom": "^0.1.27",
     "xpath": "0.0.27"
   },
diff --git a/website/static/css/custom.css b/website/static/css/custom.css
index 06e6aeb4..54db3d74 100755
--- a/website/static/css/custom.css
+++ b/website/static/css/custom.css
@@ -340,6 +340,11 @@ background: #2ecc71;
   color: #333;
 }
 
+.navSearchWrapper:before {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
 
 @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
 }


Test patch:
diff --git a/test/data/app/view/form/aria.php b/test/data/app/view/form/aria.php
new file mode 100644
index 00000000..3a9a5649
--- /dev/null
+++ b/test/data/app/view/form/aria.php
@@ -0,0 +1,136 @@
+<!doctype html>
+<html>
+    <head>
+      <title>Accessibility</title>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+      <style></style>
+    </head>
+    <body>
+    <form action="/form/complex" method="POST">
+    <div id="myBillingId">Billing</div>
+
+<div>
+    <div id="myNameId">Name</div>
+    <input type="text" name="my-form-name" aria-labelledby="myBillingId myNameId"/>
+</div>
+<div>
+    <div id="myAddressId">Address</div>
+    <input type="text" aria-label="My Address" name="my-form-address" aria-labelledby="myBillingId myAddressId"/>
+</div>
+
+<div id="myPhoneId">Phone</div>
+    <input type="text" name="my-form-phone" aria-labelledby="myPhoneId"/>
+    <input type="submit" value="Submit" />
+    </form>
+
+<div>
+    <div aria-label="get info" onclick="window.location.href='/info'">&raquo;</div>
+</div>
+
+    </body>
+</html>
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index ff0c2698..a7855296 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -213,6 +213,12 @@ module.exports.tests = function () {
       assert.ok(err);
     });
 
+    it('should should click by aria-label', async () => {
+      await I.amOnPage('/form/aria');
+      await I.click('get info');
+      await I.seeInCurrentUrl('/info');
+    });
+
     it('should click link with inner span', async () => {
       await I.amOnPage('/form/example7');
       await I.click('Buy Chocolate Bar');
@@ -455,6 +461,15 @@ module.exports.tests = function () {
       assert.equal(formContents('description'), 'Nothing special');
     });
 
+    it('should fill input by aria-label and aria-labelledby', async () => {
+      await I.amOnPage('/form/aria');
+      await I.fillField('My Address', 'Home Sweet Home');
+      await I.fillField('Phone', '123456');
+      await I.click('Submit');
+      assert.equal(formContents('my-form-phone'), '123456');
+      assert.equal(formContents('my-form-address'), 'Home Sweet Home');
+    });
+
     it('should fill textarea by overwritting the existing value', async () => {
       await I.amOnPage('/form/textarea');
       await I.fillField('Description', 'Nothing special');
diff --git a/test/unit/config_test.js b/test/unit/config_test.js
new file mode 100644
index 00000000..30ce1c15
--- /dev/null
+++ b/test/unit/config_test.js
@@ -0,0 +1,64 @@
+const config = require('../../lib/config');
+const { expect } = require('chai');
+
+describe('Config', () => {
+  beforeEach(() => config.reset());
+
+  it('should be created', () => {
+    const cfg = config.create({
+      output: './report',
+    });
+    expect(cfg).to.contain.keys(['helpers', 'plugins', 'include']);
+    expect(config.get()).to.eql(cfg);
+    expect(cfg.output).to.eql('./report');
+    expect(config.get('output')).to.eql('./report');
+    expect(config.get('output', './other')).to.eql('./report');
+    expect(config.get('tests', '**_test.js')).to.eql('**_test.js');
+  });
+
+
+  it('should be completely reset', () => {
+    config.addHook((cfg) => {
+      cfg.helpers.Puppeteer.show = true;
+    });
+    config.create({
+      tests: '**tests',
+      helpers: {
+        Puppeteer: {},
+      },
+    });
+    config.append({
+      output: './other',
+    });
+    expect(config.get('helpers').Puppeteer.show).to.eql(true);
+    config.reset();
+    expect(config.get().output).to.not.eql('./other');
+    expect(config.get()).to.not.contain.key('tests');
+    expect(config.get('helpers')).to.not.contain.key('Puppeteer');
+    config.create({
+      helpers: {
+        Puppeteer: {},
+      },
+    });
+    expect(config.get('helpers').Puppeteer.show).to.not.eql(true);
+  });
+
+  it('can be updated', () => {
+    config.create();
+    config.append({
+      output: './other',
+    });
+    expect(config.get('output')).to.eql('./other');
+  });
+
+  it('should use config hooks to enhance configs', () => {
+    config.addHook((cfg) => {
+      cfg.additionalValue = true;
+    });
+    const cfg = config.create({
+      additionalValue: false,
+    });
+    expect(cfg).to.contain.key('additionalValue');
+    expect(cfg.additionalValue).to.eql(true);
+  });
+});