Skip to content

[🐛 Bug]: What should a custom locator return if no elements are found? #16671

@yusuke-noda

Description

@yusuke-noda

Description

For example, suppose you implement a custom role-based locator like this:

const {WebDriver, By, promise: {filter}, error: {NoSuchElementError}} = require('selenium-webdriver');

function byRoleLocator(role, name) {
  return async function(context) {
    const elements = await context.findElements(By.css('*'));
    return filter(elements, async element => {
      let ariaRole = await element.getAriaRole();
      if (ariaRole === role) {
        if (name !== undefined) {
          let accessibleName = await element.getAccessibleName();
          return accessibleName === name;
        } else {
          return true;
        }
      } else {
        return false;
      }
    });
  };
}

This custom locator returns an empty array [] if no elements are found.

However, if you pass this custom locator to WebDriver.prototype.findElement() and no elements are found, an "TypeError: Custom locator did not return a WebElement" error will be thrown instead of a NoSuchElementError.

try {
  const element = await driver.findElement(byRoleLocator('textbox', 'foo')); // no elements found
} catch (e) {
  console.log(e); // TypeError is catched instead of NoSuchElemenetError
}

This is because WebDriver.prototype.findElementInternal_() returns result[0] even if the return value of the custom locator, result, is an empty array.

async findElementInternal_(locatorFn, context) {
let result = await locatorFn(context)
if (Array.isArray(result)) {
result = result[0]
}
if (!(result instanceof WebElement)) {
throw new TypeError('Custom locator did not return a WebElement')
}
return result
}

So should a custom locator throw a NoSuchElementError if no elements are found?

function byRoleLocator2(role, name) {
  return async function(context) {
    const elements = await context.findElements(By.css('*'));
    const result = await filter(elements, async element => {
      let ariaRole = await element.getAriaRole();
      if (ariaRole === role) {
        if (name !== undefined) {
          let accessibleName = await element.getAccessibleName();
          return accessibleName === name;
        } else {
          return true;
        }
      } else {
        return false;
      }
    });
    if (result.length > 0) {
      return result;
    } else {
      throw new NoSuchElementError('no elements found');
    }
  };
}

Now, if you pass this custom locator to WebDriver.prototype.findElements() and no elements are found, a NoSuchElementError will be thrown instead of returning an empty array.

try {
  const elements = await driver.findElements(byRoleLocator2('textbox', 'foo')); // Expected to return []
} catch (e) {
  console.log(e); // but NoSuchElemenetError is thrown
}

This is because WebDriver.prototype.findElements() returns [] when it catches a NoSuchElementError for non-custom locators, but does nothing for custom locators.

async findElements(locator) {
let cmd = null
if (locator instanceof RelativeBy) {
cmd = new command.Command(command.Name.FIND_ELEMENTS_RELATIVE).setParameter('args', locator.marshall())
} else {
locator = by.checkedLocator(locator)
}
if (typeof locator === 'function') {
return this.findElementsInternal_(locator, this)
} else if (cmd === null) {
cmd = new command.Command(command.Name.FIND_ELEMENTS)
.setParameter('using', locator.using)
.setParameter('value', locator.value)
}
try {
let res = await this.execute(cmd)
return Array.isArray(res) ? res : []
} catch (ex) {
if (ex instanceof error.NoSuchElementError) {
return []
}
throw ex
}
}

If a custom locator should return an empty array, modify WebDriver.prototype.findElementInternal_() to correctly handle cases where the locator's return value is an empty array.

If a custom locator should throw a NoSuchElementError, WebDriver.prototype.findElements() should return an empty array even if the custom locator throws a NoSuchElementError.

Reproducible Code

const {Builder, Browser, WebDriver, By, promise: {filter}, error: {NoSuchElementError}} = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');


function byRoleLocator(role, name) {
  return async function(context) {
    const elements = await context.findElements(By.css('*'));
    return filter(elements, async element => {
      let ariaRole = await element.getAriaRole();
      if (ariaRole === role) {
        if (name !== undefined) {
          let accessibleName = await element.getAccessibleName();
          return accessibleName === name;
        } else {
          return true;
        }
      } else {
        return false;
      }
    });
  };
}

function byRoleLocator2(role, name) {
  return async function(context) {
    const elements = await context.findElements(By.css('*'));
    const result = await filter(elements, async element => {
      let ariaRole = await element.getAriaRole();
      if (ariaRole === role) {
        if (name !== undefined) {
          let accessibleName = await element.getAccessibleName();
          return accessibleName === name;
        } else {
          return true;
        }
      } else {
        return false;
      }
    });
    if (result.length > 0) {
      return result;
    } else {
      throw new NoSuchElementError('no elements found');
    }
  };
}

(async () => {
  const driver = await new Builder().forBrowser(Browser.CHROME)
  .setChromeService(new chrome.ServiceBuilder('./chromedriver.exe'))
  .build();
  await driver.get('about:blank');
  try {
    const elm = await driver.findElement(byRoleLocator('textbox'));
  } catch (e) {
    console.log(e); // expectedd NoSuchElementError, but TypeError
  }
  try {
    const elms = await driver.findElements(byRoleLocator2('textbox'));
    console.log(elms.length); // expected '0'
  } catch (e) {
    console.log(e); // but NoSuchElementError
  }
  await driver.sleep(100);
  await driver.quit();
})();

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions