Repository: Codeception/CodeceptJS
Pull request #1870 (2019-09-02 13:27:38)

Improve handling of xpath locators with round brackets
After #1829 the locator functions `.at(i)`, `.first()` and `.last()` create xpath locators with round brackets. This may lead to "is not a valid XPath expression" exceptions (see #1866).

The changes of this PR
 * "allows xpath locators to start with round brackets" for the `isXPath()`-method
 * "prevents round brackets for nested selectors" by refactoring the old six times used  `removePrefix((new Locator(locator, 'css')).toXPath()))` expression to a new method called `convertToSubSelector(locator)` which also throws a specific error if the locator `isXPathStartingWithRoundBrackets`

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/unit/locator_test.js  2
         
```


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

Patch:
diff --git a/lib/helper/MockRequest.js b/lib/helper/MockRequest.js
new file mode 100644
index 00000000..16a647fb
--- /dev/null
+++ b/lib/helper/MockRequest.js
@@ -0,0 +1,266 @@
+const Helper = require('../helper');
+const requireg = require('requireg');
+const { appendBaseUrl } = require('../utils');
+const pollyWebDriver = require('./PollyWebDriverExt');
+
+let PollyJS;
+
+/**
+ * This helper allows to **mock requests while running tests in Puppeteer or WebDriver**.
+ * For instance, you can block calls to 3rd-party services like Google Analytics, CDNs.
+ * Another way of using is to emulate requests from server by passing prepared data.
+ *
+ * ### Installations
+ *
+ * Requires [Polly.js](https://netflix.github.io/pollyjs/#/) library by Netflix installed
+ *
+ * ```
+ * npm i @pollyjs/core @pollyjs/adapter-puppeteer --save-dev
+ * ```
+ *
+ * Requires Puppeteer helper or WebDriver helper enabled
+ *
+ * ### Configuration
+ *
+ * Just enable helper in config file:
+ *
+ * ```js
+ * helpers: {
+ *    Puppeteer: {
+ *      // regular Puppeteer config here
+ *    },
+ *    MockRequest: {}
+ * }
+ * ```
+ * The same can be done when using WebDriver helper..
+ *
+ * ### Usage
+ *
+ * Use `I.mockRequest` to intercept and mock requests.
+ *
+ */
+class MockRequest extends Helper {
+  constructor(config) {
+    super(config);
+    this._setConfig(config);
+    PollyJS = requireg('@pollyjs/core').Polly;
+    this.currentDriver = null;
+  }
+
+  static _checkRequirements() {
+    try {
+      requireg('@pollyjs/core');
+    } catch (e) {
+      return ['@pollyjs/core@^2.5.0'];
+    }
+  }
+
+  _validateConfig(config) {
+    const { url = '' } = config;
+    if (typeof url !== 'string') {
+      throw new Error(`Expected type of url(in Polly-config) is string, Found ${typeof url}.`);
+    }
+    return config;
+  }
+
+  async _after() {
+    await this.stopMocking();
+  }
+
+  /**
+   * Starts mocking of http requests.
+   * Used by mockRequest method to automatically start
+   * mocking requests.
+   *
+   * @param {*} title
+   */
+  async startMocking(title = 'Test') {
+    if (!(this.helpers && (this.helpers.Puppeteer || this.helpers.WebDriver))) {
+      throw new Error('Puppeteer and WebDriver are the only supported helpers right now');
+    }
+    if (this.helpers.Puppeteer) {
+      await this._connectPuppeteer(title);
+    } else if (this.helpers.WebDriver) {
+      await this._connectWebDriver(title);
+    }
+  }
+
+  /**
+   * Creates a polly instance by registering puppeteer adapter with the instance
+   *
+   * @param {*} title
+   */
+  async _connectPuppeteer(title) {
+    this.currentDriver = 'Puppeteer';
+    const adapter = require('@pollyjs/adapter-puppeteer');
+    PollyJS.register(adapter);
+
+    const { page } = this.helpers.Puppeteer;
+    if (!page) {
+      throw new Error('Looks like, there is no open tab');
+    }
+    await page.setRequestInterception(true);
+
+    this.polly = new PollyJS(title, {
+      mode: 'passthrough',
+      adapters: ['puppeteer'],
+      adapterOptions: {
+        puppeteer: { page },
+      },
+    });
+  }
+
+  /**
+   * Creates polly object in the browser window context using xhr and fetch adapters,
+   * after loading PollyJs and adapter scripts.
+   *
+   * @param {*} title
+   */
+  async _connectWebDriver(title) {
+    this.currentDriver = 'WebDriver';
+    const { browser } = this.helpers.WebDriver;
+    await browser.execute(pollyWebDriver.setup, title);
+    await new Promise(res => setTimeout(res, 1000));
+    this.browser = browser;
+  }
+
+  /**
+   * Mock response status
+   *
+   * ```js
+   * I.mockRequest('GET', '/api/users', 200);
+   * I.mockRequest('ANY', '/secretsRoutes/*', 403);
+   * I.mockRequest('POST', '/secrets', { secrets: 'fakeSecrets' });
+   * I.mockRequest('GET', '/api/users/1', 404, 'User not found');
+   * ```
+   *
+   * Multiple requests
+   *
+   * ```js
+   * I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
+   * ```
+   * @param {string} method request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
+   * @param {string|array} oneOrMoreUrls url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
+   * @param {number|string|object} dataOrStatusCode status code when number provided. A response body otherwise
+   * @param {string|object} additionalData response body when a status code is set by previous parameter.
+   *
+   */
+  async mockRequest(method, oneOrMoreUrls, dataOrStatusCode, additionalData = null) {
+    await this._checkAndStartMocking();
+
+    if (this.helpers.WebDriver) {
+      return this._mockRequestForWebDriver(...arguments);
+    }
+    if (this.helpers.Puppeteer) {
+      return this._mockRequestForPuppeteer(...arguments);
+    }
+  }
+
+  _mockRequestForPuppeteer(method, oneOrMoreUrls, dataOrStatusCode, additionalData) {
+    const puppeteerConfigUrl = this.helpers.Puppeteer && this.helpers.Puppeteer.options.url;
+
+    const handler = this._getRouteHandler(
+      method,
+      oneOrMoreUrls,
+      this.options.url || puppeteerConfigUrl,
+    );
+
+    if (typeof dataOrStatusCode === 'number') {
+      const statusCode = dataOrStatusCode;
+      if (additionalData) {
+        return handler.intercept((_, res) => res.status(statusCode).send(additionalData));
+      }
+      return handler.intercept((_, res) => res.sendStatus(statusCode));
+    }
+    const data = dataOrStatusCode;
+    return handler.intercept((_, res) => res.send(data));
+  }
+
+  async _mockRequestForWebDriver(method, oneOrMoreUrls, dataOrStatusCode, additionalData) {
+    const webDriverIOConfigUrl = this.helpers.WebDriver && this.helpers.WebDriver.options.url;
+    await this.browser.execute(
+      pollyWebDriver.mockRequest,
+      method,
+      oneOrMoreUrls,
+      dataOrStatusCode,
+      additionalData || undefined,
+      this.options.url || webDriverIOConfigUrl,
+    );
+  }
+
+  async _checkIfMockingStarted() {
+    if (this.currentDriver === 'Puppeteer') {
+      return (this.polly && this.polly.server);
+    }
+    if (this.currentDriver === 'WebDriver') {
+      return (this.browser && this.browser.execute(pollyWebDriver.isPollyObjectInitialized));
+    }
+  }
+
+  /**
+   * Starts mocking if it's not started yet.
+   */
+  async _checkAndStartMocking() {
+    if (!(await this._checkIfMockingStarted())) {
+      await this.startMocking();
+    }
+  }
+
+  // Get route-handler of Polly for different HTTP methods
+  // @param {string} method HTTP request methods(e.g., 'GET', 'POST')
+  // @param {string|array} oneOrMoreUrls URL or array of URLs
+  // @param {string} baseUrl hostURL
+  _getRouteHandler(method, oneOrMoreUrls, baseUrl) {
+    const { server } = this.polly;
+
+    oneOrMoreUrls = appendBaseUrl(baseUrl, oneOrMoreUrls);
+    method = method.toLowerCase();
+
+    if (httpMethods.includes(method)) {
+      return server[method](oneOrMoreUrls);
+    }
+    return server.any(oneOrMoreUrls);
+  }
+
+  /**
+   * Stops mocking requests.
+   */
+  async stopMocking() {
+    if (!(await this._checkIfMockingStarted())) return;
+
+    if (this.currentDriver === 'Puppeteer') {
+      await this._disconnectPuppeteer();
+      const { polly } = this;
+      if (!polly) return;
+      await polly.flush();
+      await polly.stop();
+
+      delete this.polly;
+    } else if (this.currentDriver === 'WebDriver') {
+      await this._disconnectWebDriver();
+    }
+  }
+
+  async _disconnectPuppeteer() {
+    const { page } = this.helpers.Puppeteer;
+    if (page) await page.setRequestInterception(false);
+  }
+
+  async _disconnectWebDriver() {
+    await this.browser.execute(pollyWebDriver.stopMocking);
+    delete this.browser;
+  }
+}
+
+const httpMethods = [
+  'get',
+  'put',
+  'post',
+  'patch',
+  'delete',
+  'merge',
+  'head',
+  'options',
+];
+
+module.exports = MockRequest;
diff --git a/lib/helper/Polly.js b/lib/helper/Polly.js
index b8ed2150..3c77d68e 100644
--- a/lib/helper/Polly.js
+++ b/lib/helper/Polly.js
@@ -1,13 +1,10 @@
-const Helper = require('../helper');
-const requireg = require('requireg');
-const { appendBaseUrl } = require('../utils');
-
-let PollyJS;
+const MockRequest = require('./MockRequest');
 
 /**
- * This helper allows to **mock requests while running tests in Puppeteer**.
- * For instance, you can block calls to 3rd-party services like Google Analytics, CDNs.
- * Another way of using is to emulate requests from server by passing prepared data.
+ * This helper works the same as MockRequest helper. It has been included for backwards compatibility
+ * reasons. So use MockRequest helper instead of this.
+ *
+ * Please refer to MockRequest helper documentation for details.
  *
  * ### Installations
  *
@@ -17,7 +14,7 @@ let PollyJS;
  * npm i @pollyjs/core @pollyjs/adapter-puppeteer --save-dev
  * ```
  *
- * Requires Puppeteer helper enabled
+ * Requires Puppeteer helper or WebDriver helper enabled
  *
  * ### Configuration
  *
@@ -31,167 +28,13 @@ let PollyJS;
  *    Polly: {}
  * }
  * ```
+ * The same can be done when using WebDriver helper..
  *
  * ### Usage
  *
  * Use `I.mockRequest` to intercept and mock requests.
  *
  */
-class Polly extends Helper {
-  constructor(config) {
-    super(config);
-    this._setConfig(config);
-    PollyJS = requireg('@pollyjs/core').Polly;
-  }
-
-  static _checkRequirements() {
-    try {
-      requireg('@pollyjs/core');
-    } catch (e) {
-      return ['@pollyjs/core@^2.5.0'];
-    }
-  }
-
-  _validateConfig(config) {
-    const { url = '' } = config;
-    if (typeof url !== 'string') {
-      throw new Error(`Expected type of url(in Polly-config) is string, Found ${typeof url}.`);
-    }
-    return config;
-  }
-
-  async _after() {
-    await this.stopMocking();
-  }
-
-  // Start mocking network requests/responses
-  async _startMocking(title = 'Test') {
-    if (!(this.helpers && this.helpers.Puppeteer)) {
-      throw new Error('Puppeteer is the only supported helper right now');
-    }
-    await this._connectPuppeteer(title);
-  }
-
-  // Connect Puppeteer helper to mock future requests.
-  async _connectPuppeteer(title) {
-    const adapter = require('@pollyjs/adapter-puppeteer');
-    PollyJS.register(adapter);
-
-    const { page } = this.helpers.Puppeteer;
-    if (!page) {
-      throw new Error('Looks like, there is no open tab');
-    }
-    await page.setRequestInterception(true);
-
-    this.polly = new PollyJS(title, {
-      mode: 'passthrough',
-      adapters: ['puppeteer'],
-      adapterOptions: {
-        puppeteer: { page },
-      },
-    });
-  }
-
-  /**
-   * Mock response status
-   *
-   * ```js
-   * I.mockRequest('GET', '/api/users', 200);
-   * I.mockRequest('ANY', '/secretsRoutes/*', 403);
-   * I.mockRequest('POST', '/secrets', { secrets: 'fakeSecrets' });
-   * I.mockRequest('GET', '/api/users/1', 404, 'User not found');
-   * ```
-   *
-   * Multiple requests
-   *
-   * ```js
-   * I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
-   * ```
-   * @param {string} method request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
-   * @param {string|array} oneOrMoreUrls url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
-   * @param {number|string|object} dataOrStatusCode status code when number provided. A response body otherwise
-   * @param {string|object} additionalData response body when a status code is set by previous parameter.
-   *
-   */
-  async mockRequest(method, oneOrMoreUrls, dataOrStatusCode, additionalData = null) {
-    await this._checkAndStartMocking();
-    const puppeteerConfigUrl = this.helpers.Puppeteer && this.helpers.Puppeteer.options.url;
-
-    const handler = this._getRouteHandler(
-      method,
-      oneOrMoreUrls,
-      this.options.url || puppeteerConfigUrl,
-    );
-
-    if (typeof dataOrStatusCode === 'number') {
-      const statusCode = dataOrStatusCode;
-      if (additionalData) {
-        return handler.intercept((_, res) => res.status(statusCode).send(additionalData));
-      }
-      return handler.intercept((_, res) => res.sendStatus(statusCode));
-    }
-    const data = dataOrStatusCode;
-    return handler.intercept((_, res) => res.send(data));
-  }
-
-  _checkIfMockingStarted() {
-    return this.polly && this.polly.server;
-  }
-
-  /**
-   * Starts mocking if it's not started yet.
-   */
-  async _checkAndStartMocking() {
-    if (!this._checkIfMockingStarted()) {
-      await this._startMocking();
-    }
-  }
-
-  // Get route-handler of Polly for different HTTP methods
-  // @param {string} method HTTP request methods(e.g., 'GET', 'POST')
-  // @param {string|array} oneOrMoreUrls URL or array of URLs
-  // @param {string} baseUrl hostURL
-  _getRouteHandler(method, oneOrMoreUrls, baseUrl) {
-    const { server } = this.polly;
-
-    oneOrMoreUrls = appendBaseUrl(baseUrl, oneOrMoreUrls);
-    method = method.toLowerCase();
-
-    if (httpMethods.includes(method)) {
-      return server[method](oneOrMoreUrls);
-    }
-    return server.any(oneOrMoreUrls);
-  }
-
-  /**
-   * Stops mocking requests.
-   */
-  async stopMocking() {
-    if (!this._checkIfMockingStarted()) return;
-    await this._disconnectPuppeteer();
-
-    const { polly } = this;
-    if (!polly) return;
-    await polly.flush();
-    await polly.stop();
-    delete this.polly;
-  }
-
-  async _disconnectPuppeteer() {
-    const { page } = this.helpers.Puppeteer;
-    if (page) await page.setRequestInterception(false);
-  }
-}
-
-const httpMethods = [
-  'get',
-  'put',
-  'post',
-  'patch',
-  'delete',
-  'merge',
-  'head',
-  'options',
-];
+class Polly extends MockRequest {}
 
 module.exports = Polly;
diff --git a/lib/helper/PollyWebDriverExt.js b/lib/helper/PollyWebDriverExt.js
new file mode 100644
index 00000000..a5e49d9e
--- /dev/null
+++ b/lib/helper/PollyWebDriverExt.js
@@ -0,0 +1,111 @@
+// @ts-nocheck
+
+const pollyWebDriver = {
+  setup: (title) => {
+    const fetchUrls = [
+      'https://cdn.jsdelivr.net/npm/@pollyjs/core@2/dist/umd/pollyjs-core.min.js',
+      'https://cdn.jsdelivr.net/npm/@pollyjs/adapter-fetch@2/dist/umd/pollyjs-adapter-fetch.min.js',
+      'https://cdn.jsdelivr.net/npm/@pollyjs/adapter-xhr@2/dist/umd/pollyjs-adapter-xhr.min.js',
+
+    ];
+
+    for (const url of fetchUrls) {
+      const script = document.createElement('script');
+      script.onload = initializePolly;
+      script.type = 'text/javascript';
+      script.src = url;
+      document.getElementsByTagName('head')[0].appendChild(script);
+    }
+
+    async function initializePolly() {
+      window.PollyJS = await window['@pollyjs/core'];
+      PollyJS.Polly.register(window['@pollyjs/adapter-fetch']);
+      PollyJS.Polly.register(window['@pollyjs/adapter-xhr']);
+      window.polly = new PollyJS.Polly(title, {
+        mode: 'passthrough',
+        adapters: ['fetch', 'xhr'],
+      });
+    }
+  },
+  mockRequest: (method, oneOrMoreUrls, dataOrStatusCode, additionalData, baseUrl) => {
+    const httpMethods = [
+      'get',
+      'put',
+      'post',
+      'patch',
+      'delete',
+      'merge',
+      'head',
+      'options',
+    ];
+
+    function getRouteHandler(method, urls, baseUrl) {
+      const { server } = window.polly;
+      urls = appendBaseUrl(baseUrl, urls);
+      method = method.toLowerCase();
+
+      if (httpMethods.includes(method)) {
+        return server[method](urls);
+      }
+      return server.any(urls);
+    }
+
+    function appendBaseUrl(baseUrl = '', oneOrMoreUrls) {
+      if (typeof baseUrl !== 'string') {
+        throw new Error(`Invalid value for baseUrl: ${baseUrl}`);
+      }
+      if (!(typeof oneOrMoreUrls === 'string' || Array.isArray(oneOrMoreUrls))) {
+        throw new Error(`Expected type of Urls is 'string' or 'array', Found '${typeof oneOrMoreUrls}'.`);
+      }
+      // Remove '/' if it's at the end of baseUrl
+      const lastChar = baseUrl.substr(-1);
+      if (lastChar === '/') {
+        baseUrl = baseUrl.slice(0, -1);
+      }
+
+      if (!Array.isArray(oneOrMoreUrls)) {
+        return joinUrl(baseUrl, oneOrMoreUrls);
+      }
+      return oneOrMoreUrls.map(url => joinUrl(baseUrl, url));
+    }
+
+    function joinUrl(baseUrl, url) {
+      return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url;
+    }
+
+    function shouldAppendBaseUrl(url) {
+      return !/^\w+\:\/\//.test(url);
+    }
+
+    function trimUrl(url) {
+      const firstChar = url.substr(1);
+      if (firstChar === '/') {
+        url = url.slice(1);
+      }
+      return url;
+    }
+
+    const handler = getRouteHandler(
+      method,
+      oneOrMoreUrls,
+      baseUrl,
+    );
+    if (typeof dataOrStatusCode === 'number') {
+      const statusCode = dataOrStatusCode;
+      if (additionalData) {
+        return handler.intercept((_, res) => res.status(statusCode).send(additionalData));
+      }
+      return handler.intercept((_, res) => res.status(statusCode));
+    }
+    const data = dataOrStatusCode;
+    return handler.intercept((_, res) => res.send(data));
+  },
+  isPollyObjectInitialized: () => window.polly && window.polly.server,
+  stopMocking: async () => {
+    await polly.flush();
+    await polly.stop();
+    delete window.polly;
+  },
+};
+
+module.exports = pollyWebDriver;
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index 1b1ecd09..a54280ce 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -439,16 +439,7 @@ class Puppeteer extends Helper {
     await this._setPage(mainPage);
     await this.closeOtherTabs();
 
-    if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
-      const dimensions = this.options.windowSize.split('x');
-      await this.resizeWindow(parseInt(dimensions[0], 10), parseInt(dimensions[1], 10));
-    }
-
     this.isRunning = true;
-
-    if (this.options.userAgent) {
-      await this.page.setUserAgent(this.options.userAgent);
-    }
   }
 
   async _stopBrowser() {
@@ -2299,7 +2290,7 @@ function $XPath(element, selector) {
   return res;
 }
 
-function targetCreatedHandler(page) {
+async function targetCreatedHandler(page) {
   if (!page) return;
   this.withinLocator = null;
   page.on('load', (frame) => {
@@ -2311,4 +2302,14 @@ function targetCreatedHandler(page) {
     this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '));
     consoleLogStore.add(msg);
   });
+
+  if (this.options.userAgent) {
+    await page.setUserAgent(this.options.userAgent);
+  }
+  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
+    const dimensions = this.options.windowSize.split('x');
+    const width = parseInt(dimensions[0], 10);
+    const height = parseInt(dimensions[1], 10);
+    await page.setViewport({ width, height });
+  }
 }
diff --git a/lib/locator.js b/lib/locator.js
index 9c427f20..c2cbc64b 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -111,17 +111,17 @@ class Locator {
   }
 
   find(locator) {
-    const xpath = sprintf('%s//%s', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s//%s', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 
   withChild(locator) {
-    const xpath = sprintf('%s[./child::%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s[./child::%s]', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 
   withDescendant(locator) {
-    const xpath = sprintf('%s[./descendant::%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s[./descendant::%s]', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 
@@ -166,17 +166,17 @@ class Locator {
   }
 
   inside(locator) {
-    const xpath = sprintf('%s[ancestor::%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s[ancestor::%s]', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 
   after(locator) {
-    const xpath = sprintf('%s[preceding-sibling::%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s[preceding-sibling::%s]', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 
   before(locator) {
-    const xpath = sprintf('%s[following-sibling::%s]', this.toXPath(), removePrefix((new Locator(locator, 'css')).toXPath()));
+    const xpath = sprintf('%s[following-sibling::%s]', this.toXPath(), convertToSubSelector(locator));
     return new Locator({ xpath });
   }
 }
@@ -269,10 +269,24 @@ function isAccessibility(locator) {
 }
 
 function isXPath(locator) {
-  return locator.substr(0, 2) === '//' || locator.substr(0, 2) === './';
+  const trimmed = locator.replace(/^\(+/, '').substr(0, 2);
+  return trimmed === '//' || trimmed === './';
+}
+
+function isXPathStartingWithRoundBrackets(locator) {
+  return isXPath(locator) && locator[0] === '(';
 }
 
 function removePrefix(xpath) {
   return xpath
     .replace(/^(\.|\/)+/, '');
 }
+
+function convertToSubSelector(locator) {
+  const xpath = (new Locator(locator, 'css')).toXPath();
+  if (isXPathStartingWithRoundBrackets(xpath)) {
+    throw new Error('XPath with round brackets is not possible here! '
+      + 'May be a nested locator with at() last() or first() causes this error.');
+  }
+  return removePrefix(xpath);
+}


Test patch:
diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js
index 83890d6a..56badbc6 100644
--- a/test/acceptance/codecept.Puppeteer.js
+++ b/test/acceptance/codecept.Puppeteer.js
@@ -15,7 +15,7 @@ module.exports.config = {
         ],
       },
     },
-    Polly: {},
+    MockRequest: {},
   },
   include: {},
   bootstrap: false,
diff --git a/test/acceptance/codecept.WebDriver.js b/test/acceptance/codecept.WebDriver.js
index 7a1e0e3d..574ea9b5 100644
--- a/test/acceptance/codecept.WebDriver.js
+++ b/test/acceptance/codecept.WebDriver.js
@@ -17,7 +17,7 @@ module.exports.config = {
       //   },
       // },
     },
-
+    MockRequest: {},
   },
   include: {},
   bootstrap: done => setTimeout(done, 5000), // let's wait for selenium
diff --git a/test/acceptance/codecept.WebDriverIO.js b/test/acceptance/codecept.WebDriverIO.js
index 52d25a9c..ef85822f 100644
--- a/test/acceptance/codecept.WebDriverIO.js
+++ b/test/acceptance/codecept.WebDriverIO.js
@@ -17,7 +17,6 @@ module.exports.config = {
       //   },
       // },
     },
-
   },
   include: {},
   bootstrap: done => setTimeout(done, 5000), // let's wait for selenium
diff --git a/test/acceptance/mock_test.js b/test/acceptance/mock_test.js
index 3bd8e1fd..7af4d302 100644
--- a/test/acceptance/mock_test.js
+++ b/test/acceptance/mock_test.js
@@ -6,7 +6,7 @@ const fetchComments = response => response.url() === 'https://jsonplaceholder.ty
 
 const fetchUsers = response => response.url() === 'https://jsonplaceholder.typicode.com/users/1';
 
-Scenario('change statusCode @Puppeteer', (I) => {
+Scenario('change statusCode @Puppeteer @WebDriver', (I) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', 404);
   I.click('GET POSTS');
@@ -14,7 +14,7 @@ Scenario('change statusCode @Puppeteer', (I) => {
   I.stopMocking();
 });
 
-Scenario('change response data @Puppeteer', (I) => {
+Scenario('change response data @Puppeteer @WebDriver', (I) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
     modified: 'This is modified from mocking',
@@ -24,7 +24,7 @@ Scenario('change response data @Puppeteer', (I) => {
   I.stopMocking();
 });
 
-Scenario('change response data for multiple requests @Puppeteer', (I) => {
+Scenario('change response data for multiple requests @Puppeteer @WebDriver', (I) => {
   I.amOnPage('/form/fetch_call');
   I.mockRequest(
     'GET',
@@ -47,7 +47,7 @@ Scenario('change response data for multiple requests @Puppeteer', (I) => {
 });
 
 Scenario(
-  'should request for original data after mocking stopped @Puppeteer',
+  'should request for original data after mocking stopped @Puppeteer @WebDriver',
   (I) => {
     I.amOnPage('/form/fetch_call');
     I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
diff --git a/test/unit/locator_test.js b/test/unit/locator_test.js
index f068f5e2..1a095db6 100644
--- a/test/unit/locator_test.js
+++ b/test/unit/locator_test.js
@@ -156,7 +156,7 @@ describe('Locator', () => {
       .find('td')
       .first();
     const nodes = xpath.select(l.toXPath(), doc);
-    expect(nodes).to.have.length(1);
+    expect(nodes).to.have.length(1, l.toXPath());
     expect(nodes[0].firstChild.data).to.eql('Show');
   });
 
@@ -188,6 +188,18 @@ describe('Locator', () => {
     expect(nodes).to.have.length(1, l.toXPath());
   });
 
+  it('should throw an error when xpath with round brackets is nested', () => {
+    assert.throws(() => {
+      Locator.build('tr').find('(./td)[@id="id"]');
+    }, /round brackets/);
+  });
+
+  it('should throw an error when locator with specific position is nested', () => {
+    assert.throws(() => {
+      Locator.build('tr').withChild(Locator.build('td').first());
+    }, /round brackets/);
+  });
+
   it('should not select element by deep nested siblings', () => {
     const l = Locator.build('//table')
       .withChild('td');