-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Description
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.
selenium/javascript/selenium-webdriver/lib/webdriver.js
Lines 1045 to 1054 in a94820f
| 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.
selenium/javascript/selenium-webdriver/lib/webdriver.js
Lines 1057 to 1081 in a94820f
| 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();
})();