selenium icon indicating copy to clipboard operation
selenium copied to clipboard

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

Open yusuke-noda opened this issue 1 month ago • 1 comments

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.

https://github.com/SeleniumHQ/selenium/blob/a94820fa730cf3f80d63d7ca22976513372d275d/javascript/selenium-webdriver/lib/webdriver.js#L1045-L1054

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.

https://github.com/SeleniumHQ/selenium/blob/a94820fa730cf3f80d63d7ca22976513372d275d/javascript/selenium-webdriver/lib/webdriver.js#L1057-L1081

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();
})();

yusuke-noda avatar Dec 03 '25 05:12 yusuke-noda

@yusuke-noda, thank you for creating this issue. We will troubleshoot it as soon as we can.

Selenium Triage Team: remember to follow the Triage Guide

selenium-ci avatar Dec 03 '25 05:12 selenium-ci