test: simplify SeleniumHelperError

This commit is contained in:
Christopher Willis-Ford 2023-10-24 10:22:58 -07:00
parent 4fb4eaab76
commit 8cf09452c6

View file

@ -37,58 +37,10 @@ Error.prepareStackTrace = function (error, stack) {
}; };
/** /**
* An error that can be chained to another error. * An error thrown by SeleniumHelper.
* @extends Error * @extends Error
*/ */
class ChainableError extends Error { class SeleniumHelperError extends Error {
/**
* Instantiate a new ChainableError.
* @param {string} message The error message for this layer.
*/
constructor (message) {
super(message);
this.baseMessage = message;
Object.setPrototypeOf(this, ChainableError.prototype); // see https://stackoverflow.com/a/41102306
this.name = 'ChainableError';
Error.captureStackTrace(this, this.constructor);
}
/**
* Add a new layer to the error chain.
* @param {Error} innerError The error to add to the chain.
* @returns {ChainableError} This error, with the new layer added.
*/
chain (innerError) {
this.innerError = innerError;
const messages = [];
this.collectMessages(messages);
this.message = messages.join('\n');
return this;
}
/**
* Collect all the error messages in the chain.
* @param {Array<string>} lines The array to collect the messages into.
*/
collectMessages (lines) {
lines.push(this.baseMessage);
if (this.innerError) {
if (this.innerError.collectMessages) {
this.innerError.collectMessages(lines);
} else {
lines.push(this.innerError.baseMessage || this.innerError.message);
}
}
}
}
/**
* An error thrown by SeleniumHelper.
* @extends ChainableError
*/
class SeleniumHelperError extends ChainableError {
/** /**
* Instantiate a new SeleniumHelperError. * Instantiate a new SeleniumHelperError.
* @param {string} message The error message for this layer. * @param {string} message The error message for this layer.
@ -98,8 +50,7 @@ class SeleniumHelperError extends ChainableError {
* try { * try {
* doThings(); * doThings();
* } catch (inner) { * } catch (inner) {
* await e.collectContext(driver); * throw await e.chain(inner, driver);
* throw e.chain(inner);
* } * }
*/ */
constructor (message, kvList = []) { constructor (message, kvList = []) {
@ -114,44 +65,57 @@ class SeleniumHelperError extends ChainableError {
} }
/** /**
* Collect error context from the webdriver. * Add a new layer to the error chain.
* @param {webdriver.ThenableWebDriver} driver The webdriver instance. * Collects context from the webdriver if it is present AND this is the innermost `SeleniumHelperError`.
* @returns {Promise} A promise that resolves when the context is collected. * @param {Error|SeleniumHelperError} innerError The error to add to the chain.
* @param {webdriver.ThenableWebDriver} [driver] Optional webdriver instance to collect context from.
* @returns {Promise<SeleniumHelperError>} This error, with the new layer added.
*/ */
async collectContext (driver) { async chain (innerError, driver) {
// It would be really nice to wait until `message` time to collect all this information, const messageLines = [
// but that's not an option because of all these async calls. this.message,
const url = await driver.getCurrentUrl(); innerError.message
const title = await driver.getTitle();
// const pageSource = await driver.getPageSource();
const browserLogEntries = await driver.manage()
.logs()
.get('browser');
const browserLogText = browserLogEntries.map(entry => entry.message).join('\n');
this.contextLines = [
`Browser URL: ${url}`,
`Browser title: ${title}`,
`Browser logs:\n*****\n${browserLogText}\n*****`
// `Browser page source:\n*****\n${pageSource}\n*****`
]; ];
} // If the inner error has already collected context, don't collect it again.
if (driver && !(innerError && innerError.collectContext)) {
chain (innerError) { await this.collectContext(messageLines, driver);
super.chain(innerError); // rebuilds the message
let messageLines = [
this.message
];
if (this.innerError && this.innerError.contextLines) {
this.contextLines = this.innerError.contextLines;
}
if (this.contextLines) {
messageLines = messageLines.concat(this.contextLines);
} else {
messageLines.push('(no webdriver context collected)');
} }
this.message = messageLines.join('\n'); this.message = messageLines.join('\n');
return this; return this;
} }
/**
* Collect error context from the webdriver.
* @param {Array<string>} messageLines Add context lines to this array.
* @param {webdriver.ThenableWebDriver} driver The webdriver instance to collect context from.
* @returns {Promise} A promise that resolves when the context is collected.
*/
async collectContext (messageLines, driver) {
// It would be really nice to wait until `message` time to collect all this information,
// but that's not an option because of all these async calls.
const [
url,
title,
// pageSource,
logEntries
] = await Promise.all([
driver.getCurrentUrl(),
driver.getTitle(),
// driver.getPageSource(),
driver.manage()
.logs()
.get('browser')
]);
messageLines.push(
`Browser URL: ${url}`,
`Browser title: ${title}`,
'Browser logs:',
'*****',
...logEntries.map(entry => entry.message),
'*****'
// 'Browser page source:', '*****', pageSource, '*****'
);
}
} }
class SeleniumHelper { class SeleniumHelper {
@ -295,8 +259,7 @@ class SeleniumHelper {
await this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS); await this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS);
return el; return el;
} catch (cause) { } catch (cause) {
await outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -309,8 +272,7 @@ class SeleniumHelper {
try { try {
await this.driver.wait(until.stalenessOf(element), DEFAULT_TIMEOUT_MILLISECONDS); await this.driver.wait(until.stalenessOf(element), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) { } catch (cause) {
await outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -325,8 +287,7 @@ class SeleniumHelper {
const el = await this.findByXpath(xpath); const el = await this.findByXpath(xpath);
await el.click(); await el.click();
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -340,8 +301,7 @@ class SeleniumHelper {
try { try {
await this.clickXpath(`//*[contains(text(), '${text}')]`); await this.clickXpath(`//*[contains(text(), '${text}')]`);
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -358,8 +318,7 @@ class SeleniumHelper {
DEFAULT_TIMEOUT_MILLISECONDS DEFAULT_TIMEOUT_MILLISECONDS
); );
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -373,8 +332,7 @@ class SeleniumHelper {
try { try {
await this.clickXpath(`//button[contains(text(), '${text}')]`); await this.clickXpath(`//button[contains(text(), '${text}')]`);
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -388,8 +346,7 @@ class SeleniumHelper {
try { try {
return await this.driver.wait(until.elementLocated(By.css(css)), DEFAULT_TIMEOUT_MILLISECONDS); return await this.driver.wait(until.elementLocated(By.css(css)), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -404,8 +361,7 @@ class SeleniumHelper {
const el = await this.findByCss(css); const el = await this.findByCss(css);
await el.click(); await el.click();
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -424,8 +380,7 @@ class SeleniumHelper {
.dragAndDrop(startEl, endEl) .dragAndDrop(startEl, endEl)
.perform(); .perform();
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -448,8 +403,7 @@ class SeleniumHelper {
await word.sendKeys(password + this.getKey('ENTER')); await word.sendKeys(password + this.getKey('ENTER'));
await this.findByXpath('//span[contains(@class, "profile-name")]'); await this.findByXpath('//span[contains(@class, "profile-name")]');
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -463,8 +417,7 @@ class SeleniumHelper {
try { try {
await this.driver.wait(until.urlMatches(regex), DEFAULT_TIMEOUT_MILLISECONDS); await this.driver.wait(until.urlMatches(regex), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -496,8 +449,7 @@ class SeleniumHelper {
return true; return true;
}); });
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -514,8 +466,7 @@ class SeleniumHelper {
const classList = classes.split(' '); const classList = classes.split(' ');
return classList.includes(cl); return classList.includes(cl);
} catch (cause) { } catch (cause) {
outerError.collectContext(this.driver); throw await outerError.chain(cause, this.driver);
throw outerError.chain(cause);
} }
} }
@ -529,8 +480,7 @@ class SeleniumHelper {
try { try {
await driver.wait(until.elementIsVisible(element), DEFAULT_TIMEOUT_MILLISECONDS); await driver.wait(until.elementIsVisible(element), DEFAULT_TIMEOUT_MILLISECONDS);
} catch (cause) { } catch (cause) {
outerError.collectContext(driver); throw await outerError.chain(cause, driver);
throw outerError.chain(cause);
} }
} }
} }