dce-selenium
Version:
Selenium library to simplify testing and automatically snapshot the DOM.
1,281 lines (1,168 loc) • 40.6 kB
JavaScript
// Import dependencies
const { By, until } = require('selenium-webdriver');
const prompt = require('prompt-sync')();
const queryString = require('query-string');
const path = require('path');
// Import helpers
const saveScreenshotAndSource = require('./snapshots/saveScreenshotAndSource');
const saveJSON = require('./snapshots/saveJSON');
const saveText = require('./snapshots/saveText');
const sendRequest = require('./sendRequest');
const consoleLog = require('../consoleLog');
/**
* Replaces all occurrences in a string
*/
const replaceAll = (str, search, replacement) => {
return str.replace(new RegExp(search, 'g'), replacement);
};
/**
* Fixes a url (adds protocol and host)
*/
const fixURL = (url) => {
if (url.startsWith('http')) {
// URL is complete!
return url;
}
// Need to add a host and protocol
const protocol = (
global.dceSeleniumConfig.dontUseHTTPS
? 'http://'
: 'https://'
);
if (!global.dceSeleniumConfig.defaultHost) {
consoleLog('\nOops! We can\'t visit the following url without a hostname:');
consoleLog(url);
consoleLog('Either include a hostname in the url or initialize with a defaultHost');
process.exit(0);
}
return `${protocol}${global.dceSeleniumConfig.defaultHost}${url.startsWith('/') ? '' : '/'}${url}`;
};
/**
* Makes sure all required arguments are included, throws an error if any are
* excluded
* @param {object} args - an object where keys are the names of arguments and
* values are the respective values of those arguments
*/
const ensureIncluded = (args) => {
const funcName = `Driver.${global.actionTitle}`;
if (args === undefined) {
throw new Error(`${funcName} missing required arguments.`);
}
// Find the missing arguments
const missingArgs = (
Object.keys(args)
.filter((name) => {
return (args[name] === undefined);
})
);
// Create an error
if (missingArgs.length > 0) {
const prefix = (
missingArgs.length === 1
? 'missing required argument'
: 'missing required arguments'
);
const error = new Error(`${funcName} ${prefix} ${JSON.stringify(missingArgs)}`);
throw error;
}
};
/*------------------------------------------------------------------------*/
/* Snapshots */
/*------------------------------------------------------------------------*/
// Types of snapshots
const SNAPSHOT_TYPES = {
JSON: 'json', // Save output of the function as JSON
SCREENSHOT_AND_SOURCE: 'screenshot-and-source', // Screenshot & save page src
TEXT: 'text', // Save output of the function as text
};
// To add snapshots to a command, add another parameter to your Driver function
// Driver.funcName.snapshotType = {
// before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE, <-- optional
// during: SNAPSHOT_TYPES.TEXT <or> SNAPSHOT_TYPES.JSON, <-- options
// after: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE, <-- optional
// };
/*------------------------------------------------------------------------*/
/* Driver Class */
/*------------------------------------------------------------------------*/
// NOTE: to add a new driver function, do not add them to the class
// Instead, add them as static functions
// Driver.funcName = async function (param1, param2) {
// ...
// };
class Driver {
constructor(webdriver, log) {
this.webdriver = webdriver;
this.log = log || (() => {});
// Surround static functions with pre/post
Object.keys(Driver).forEach((prop) => {
const baseFunction = Driver[prop].bind(this);
// Add function and bind it
this[prop] = async (...args) => {
// Take snapshot before (if applicable)
await this._pre(prop);
// Run the function
const ret = await baseFunction(...args);
// Take snapshot during (if applicable)
if (global.actionTitle === prop) {
// Take snapshot
const snapshotType = (
Driver[prop].snapshotType
? Driver[prop].snapshotType.during
: null
);
if (snapshotType === SNAPSHOT_TYPES.JSON) {
await saveJSON(ret);
} else if (snapshotType === SNAPSHOT_TYPES.TEXT) {
await saveText(ret);
}
}
// Take snapshot after (if applicable)
await this._post(prop);
// Return the value returned from the base function
return ret;
};
});
}
/**
* Function to be called before each command
*/
async _pre(prop) {
if (global.actionTitle) {
return;
}
// Save the action title so we the snapshot filenames are valid
global.actionTitle = prop;
// Take snapshot
const snapshotType = (
Driver[prop].snapshotType
? Driver[prop].snapshotType.before
: null
);
if (snapshotType === SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE) {
await saveScreenshotAndSource(this.webdriver, 'before');
}
}
/**
* Function to be called after each command
*/
async _post(prop) {
if (global.actionTitle !== prop) {
// We're currently inside of a function that was called by another
// function (do not log this)
return;
}
// Take snapshot
const snapshotType = (
Driver[prop].snapshotType
? Driver[prop].snapshotType.post
: null
);
if (snapshotType === SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE) {
await saveScreenshotAndSource(this.webdriver, 'post');
}
// Remove the title for the next test
global.actionTitle = null;
}
/**
* Quit webdriver (close automated browser window)
*/
async quit() {
// Don't quit again if already dead
if (this.dead) {
return;
}
// Attempt to quit the webdriver
try {
await this.webdriver.quit();
this.dead = true;
} catch (err) {
// Session already dead. No error here
}
}
}
// Allow bindable async functions
/* eslint-disable func-names */
/*------------------------------------------------------------------------*/
/* Navigation */
/*------------------------------------------------------------------------*/
/**
* Visits a location (path/url). Prepends defaultHost if no host included and
* prepends https:// if no protocol included.
* @param {string} location - the path or url to visit
* @param {string|string[]} [locationToWaitFor=location] - the location or
* list of locations to wait for before resolving. If the location we're
* visiting redirects the user elsewhere, we need to wait for that location
* instead. Put that final location as this locationToWaitFor. If there are
* more than one possible final locations, include an array
* @return {Promise}
*/
Driver.visit = async function (location, finalLocation) {
ensureIncluded({ location });
this.log(`visit ${location}`);
const url = fixURL(location);
const ret = await this.webdriver.get(url);
if (finalLocation) {
this.log('waiting for redirect to final location');
await this.waitForLocations(finalLocation);
}
return ret;
};
Driver.visit.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Visits a location (path/url) with POST. Prepends defaultHost if no host
* included and prepends https:// if no protocol included.
* @param {string} location - the path or url to visit
* @param {object} [body={}] - the POST request body to send
* @param {string|string[]} [locationToWaitFor=location] - the location or
* list of locations to wait for before resolving. If the location we're
* visiting redirects the user elsewhere, we need to wait for that location
* instead. Put that final location as this locationToWaitFor. If there are
* more than one possible final locations, include an array
* @return {Promise}
*/
Driver.post = async function (location, body = {}, finalLocation = null) {
ensureIncluded({ location });
this.log(`post ${location}`);
const url = fixURL(location);
await sendRequest({
url,
body,
driver: this,
method: 'POST',
});
if (finalLocation) {
this.log('waiting for redirect to final location');
await this.waitForLocations(finalLocation);
}
};
Driver.post.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/*------------------------------------------------------------------------*/
/* Session */
/*------------------------------------------------------------------------*/
/**
* Resets the session and navigates back to about:blank
*/
Driver.reset = async function () {
await this.webdriver.manage().deleteAllCookies();
await this.webdriver.get('about:blank');
};
/*------------------------------------------------------------------------*/
/* Waiting */
/*------------------------------------------------------------------------*/
/**
* Wait for user to hit "enter" in terminal
*/
Driver.pause = async function () {
return new Promise((resolve) => {
const text = '\u2551 > Execution Paused! Press enter to continue.';
if (prompt(text) === null) {
// User pressed ctrl+c
process.exit(0);
}
resolve();
});
};
/**
* Wait for specific amount of time
* @param {number} ms - number of ms to wait
*/
Driver.wait = async function (ms = 0) {
ensureIncluded({ ms });
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
/**
* Wait for an element to be visible
* @param {string} selectorOrElem - the css selector or the element
* @param {number} [timeout=10000] - the number of ms to wait before timing out
*/
Driver.waitForElementVisible = (
async function (selectorOrElem, timeout = 10000) {
ensureIncluded({ selectorOrElem });
let el;
if (typeof selectorOrElem === 'string') {
this.log(`wait for ${selectorOrElem} to be visible`);
const by = By.css(selectorOrElem);
await this.webdriver.wait(until.elementLocated(by, timeout));
el = this.webdriver.findElement(by);
} else {
this.log('wait for element to be visible');
el = selectorOrElem;
}
return this.webdriver.wait(until.elementIsVisible(el), timeout);
}
);
/**
* Wait for an element with contents to be visible
* @param {object} options - an object holding all arguments
* @param {string} options.contents - the element contents to search for
* @param {string} [options.selector] - a css selector to limit the search
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
* @param {number} [options.timeout=10000] - the number of ms to wait before
* timing out
*/
Driver.waitForElementWithContentsVisible = (
async function (...args) {
let contents;
let selector;
let timeout;
if (typeof args[0] === 'object') {
({ contents, selector } = args[0]);
timeout = args[0].timeout || 10000;
} else {
contents = args[0];
selector = args[1];
timeout = args[2] || 10000;
}
ensureIncluded({ contents });
// Print log message
if (selector) {
this.log(`wait for item with contents "${contents}" and selector "${selector}" to be visible`);
} else {
this.log(`wait for item with contents "${contents}" to be visible`);
}
const by = await this._genContentsByObject(contents, selector);
await this.webdriver.wait(until.elementLocated(by, timeout));
const el = this.webdriver.findElement(by);
return this.webdriver.wait(until.elementIsVisible(el), timeout);
}
);
/**
* Wait for the browser location (path/url) to match
* @param {string} location - location template string. Use form:
* /course/:courseid/assignment/:assignmentid where parts starting with ":"
* may be replaced with anything
* @param {number} [timeout=10000] - the number of ms to wait before timing out
*/
Driver.waitForLocation = async function (location, timeout = 10000) {
ensureIncluded({ location });
const startTime = Date.now();
let webPath;
let regStr;
if (!location.startsWith('http')) {
// Path
webPath = location;
if (!global.dceSeleniumConfig.defaultHost) {
consoleLog('\nCannot wait for path:');
consoleLog(location);
consoleLog('No host was included in the location and we have no default host');
process.exit(0);
}
// Use defaults for location and defaultHost
regStr = `^http${global.dceSeleniumConfig.dontUseHTTPS ? '' : 's'}://${global.dceSeleniumConfig.defaultHost}`;
} else {
// URL
try {
const urlSections = location.split('/');
const host = urlSections[2];
webPath = location.split(host)[1];
regStr = `^${location.split(host)[0]}${host}`;
} catch (err) {
consoleLog('\nCannot wait for url: it is invalid');
consoleLog(err.message);
process.exit(0);
}
}
// Add in path, part by part
const parts = webPath.substring(1).split('/');
parts.forEach((part) => {
if (part.startsWith(':')) {
// This is a placeholder
regStr += '/[^/]*';
} else {
// This is a defined part of the url
regStr += '/' + part;
}
});
if (location.includes('?')) {
// Require the proper query string
regStr = regStr.replace('?', '\\?') + '$';
} else {
// Add ending that ignores query strings and following "/"
regStr += '[/]?(?:\\?[^]*|$)';
}
// Wait until url matches
await this.webdriver.wait(until.urlMatches(new RegExp(regStr)), timeout);
const currentURL = await this.webdriver.getCurrentUrl();
// Wait until page is ready
await new Promise((resolve, reject) => {
let done;
const checkAgain = async () => {
if (done) {
return;
}
// Make sure browser is loaded and at correct location
const windowURL = await this.webdriver.executeScript('return window.location.href');
const readyState = await this.webdriver.executeScript('return document.readyState');
const ready = (readyState === 'complete' && windowURL === currentURL);
const msElapsed = Date.now() - startTime;
// Check if done
if (ready) {
done = true;
return resolve();
}
// Check for timeout
if (msElapsed >= timeout) {
done = true;
return reject(new Error(`We reached a timeout (${timeout}ms) while waiting for the page to be ready.`));
}
// Not done or timed out. Try again
setTimeout(checkAgain, 15);
};
checkAgain();
});
};
/**
* Wait for the browser location (path/url) to match any
* @param {string[]} locations - list of location template string. Use form:
* /course/:courseid/assignment/:assignmentid where parts starting with ":"
* may be replaced with anything
* @param {number} [timeout=10000] - the number of ms to wait before timing out
*/
Driver.waitForLocations = async function (locations, timeout = 10000) {
ensureIncluded({ locations });
// Ensure included is an array
const finalLocations = (
Array.isArray(locations)
? locations
: [locations]
);
// Start promises
let waitPromises = finalLocations.map((loc) => {
return this.waitForLocation(loc, timeout);
});
// Wait an if any succeed, resolve. If none succeed, throw a custom error
return new Promise((resolve, reject) => {
// if any succeed, resolve
waitPromises = waitPromises.map((prom) => {
return prom.then(resolve);
});
// If all fail, reject
const promisesThatResolveOnFailure = waitPromises.map((prom) => {
return new Promise((r) => {
prom.catch(r);
});
});
Promise.all(promisesThatResolveOnFailure).then(() => {
const errorMessage = `We waited for ${finalLocations.length === 1 ? 'a' : 'any of ' + finalLocations.length} location${finalLocations.length === 1 ? '' : 's'} to load but it did not finished loading within the timeout.`;
return reject(new Error(errorMessage));
});
});
};
/*------------------------------------------------------------------------*/
/* Element Finding */
/*------------------------------------------------------------------------*/
/**
* Create's a By instance for using in finding an element
* @param {string} contents - the element contents to search for
* @param {string} [selector] - a css selector to limit the search
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
*
* @return {By} Selenium By instance
*/
Driver._genContentsByObject = async function (contents, selector) {
let start = '*';
if (selector) {
if (selector.startsWith('#')) {
// id
start = `*[@id='${selector.substring(1)}']`;
} else if (selector.startsWith('.')) {
// class
start = `*[contains(@class,'${selector.substring(1)}')]`;
} else if (selector.startsWith('*[')) {
// element with attribute
start = `*[@${selector.substring(2)}]`;
} else if (selector === '*') {
// wildcard
start = '*';
} else {
// just a tag names
const tagName = selector.split('>')[0].split('+')[0];
if (selector.includes('+')) {
// assume looking for next sibling
start = `${tagName}/following-sibling::*[0]`;
} else if (selector.includes('>')) {
if (selector.includes('first-child')) {
// assume looking for first child of tagName
start = `${tagName}/*[0]`;
} else {
// assume looking for all children
start = `${tagName}/*`;
}
} else {
// assume just a tag name
start = tagName;
}
}
}
// Build the xpath
let contentsEscaped;
if (!contents.includes('\'')) {
contentsEscaped = `'${contents}'`;
} else if (!contents.includes('"')) {
contentsEscaped = `"${contents}"`;
} else {
contentsEscaped = `concat('${replaceAll(contents, '\'', '\',"\'", \'')}')`;
}
const xpath = `//${start}[text()[contains(.,${contentsEscaped})]]`;
return By.xpath(xpath);
};
/**
* Find an element by css selector
* @param {string} selector - a css selector
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
*
* @return {WebElement} Selenium WebElement
*/
Driver.getElement = async function (selector) {
ensureIncluded({ selector });
this.log(`get element ${selector}`);
return this.webdriver.findElement(By.css(selector));
};
/**
* Find all elements that match a css selector
* @param {string} selector - a css selector
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
*
* @return {WebElement[]} list of Selenium WebElements
*/
Driver.getElements = async function (selector) {
return this.webdriver.findElements(By.css(selector));
};
/**
* Find an element by its contents and optionally by a css selector
* @param {string} contents - the element contents to search for
* @param {string} [selector] - a css selector to limit the search
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
*
* @return {WebElement} Selenium WebElement
*/
Driver.getElementByContents = async function (contents, selector) {
ensureIncluded({ contents });
const by = await this._genContentsByObject(contents, selector);
return this.webdriver.findElement(by);
};
/**
* If given a selector, will find the element that matches a selector. If given
* an element, just returns the element
* @param {string|WebElement} selectorOrElement - the css selector or the
* element
* @return {WebElement} the web element
*/
Driver._makeSureIsElement = async function (selectorOrElement) {
if (typeof selectorOrElement === 'string') {
// This is a selector. Find the element
return this.webdriver.findElement(By.css(selectorOrElement));
}
// This is already an element. Just return it.
return selectorOrElement;
};
/**
* Given a css selector or element, finds the parent of the elemnt
* @param {string|WebElement} selectorOrElement - the css selector or the
* element
* @return {WebElement}
*/
Driver.parentOf = async function (selectorOrElement) {
ensureIncluded({ selectorOrElement });
const element = await this._makeSureIsElement(selectorOrElement);
const parent = await element.findElement(By.xpath('./..'));
return parent;
};
/**
* Given a parent element and a css selector, finds the first descendent of
* @param {WebElement} parent - the parent element
* @param {string} selector - css selector
* @return {WebElement}
*/
Driver.descendantOf = async function (parent, selector) {
ensureIncluded({ parent, selector });
const element = await this._makeSureIsElement(parent);
const descendant = await element.findElement(By.css(selector));
return descendant;
};
/*------------------------------------------------------------------------*/
/* Interactions */
/*------------------------------------------------------------------------*/
/**
* Finds an element by its css selector, then clicks it
* @param {string|WebElement} selectorOrElem - the css selector of the element
* to click or the element to click
*/
Driver.click = async function (selector) {
ensureIncluded({ selector });
if (typeof selector === 'string') {
this.log(`click "${selector}"`);
} else {
this.log('click element');
}
const elem = await this._makeSureIsElement(selector);
const ret = await elem.click();
return ret;
};
Driver.click.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Finds an anchor tag and forces it to open in the same tab
* @param {string|WebElement} selectorOrElem - the css selector of the anchor
* tag to follow
* @param {string|string[]} [locationToWaitFor=location] - the location or
* list of locations to wait for before resolving. If the location we're
* visiting redirects the user elsewhere, we need to wait for that location
* instead. Put that final location as this locationToWaitFor. If there are
* more than one possible final locations, include an array
*/
Driver.openAnchorInSameTab = async function (selector, finalLocation) {
ensureIncluded({ selector });
if (typeof selector === 'string') {
this.log(`open anchor "${selector}" in same tab`);
} else {
this.log('open anchor in same tab');
}
const elem = await this._makeSureIsElement(selector);
const url = await elem.getAttribute('href');
await this.visit(url, finalLocation);
return elem;
};
Driver.openAnchorInSameTab.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Click by contents
* @param {string} contents - the text contents of the item to click
* @param {string} [selector] - the css selector to narrow down the search
*/
Driver.clickByContents = async function (contents, selector) {
ensureIncluded({ contents });
const ending = (
selector
? ` and selector "${selector}"`
: ''
);
this.log(`click by contents "${contents}"${ending}`);
const elem = await this.getElementByContents(contents, selector);
const ret = await elem.click();
return ret;
};
Driver.clickByContents.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Types text into a field
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or the element to type into
* @param {string} text - the text to type
*/
Driver.typeInto = async function (selectorOrElem, text) {
ensureIncluded({ selectorOrElem, text });
if (typeof selectorOrElem === 'string') {
this.log(`type "${text}" into ${selectorOrElem}`);
} else {
this.log(`type "${text}" into a field`);
}
const elem = await this._makeSureIsElement(selectorOrElem);
return elem.sendKeys(text);
};
Driver.typeInto.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Simulate keystrokes into the page document,
* @param {string} text - the text to type
* @param {boolean} [finishWithEnter] - if true, the enter key is pressed
* after the text is typed
*/
Driver.simulateKeystrokesIntoDocument = async function (text, finishWithEnter) {
ensureIncluded({ text });
const timeBetweenKeystrokes = 10;
await this.webdriver.executeScript(`
(() => {
let time = 0;
const timeBetweenKeystrokes = ${timeBetweenKeystrokes};
const finishWithEnterKey = ${!!finishWithEnter};
const text = '${text}';
const textChars = String(text).split('');
textChars.forEach((char, i) => {
// Type the key
setTimeout(() => {
const keyEvent = new CustomEvent('keydown');
keyEvent.key = char;
document.dispatchEvent(keyEvent);
}, time);
time += timeBetweenKeystrokes;
});
// Hit enter if applicable
if (finishWithEnterKey) {
setTimeout(() => {
const keyEvent = new CustomEvent('keydown');
keyEvent.key = 'Enter';
document.dispatchEvent(keyEvent);
}, time);
}
})();
`);
// Wait for process to finish
await new Promise((r) => {
setTimeout(r, timeBetweenKeystrokes * (text.length + 1));
});
};
Driver.simulateKeystrokesIntoDocument.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Scrolls to an element
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* element to scroll to, or the WebElement to scroll to
*/
Driver.scrollTo = async function (selectorOrElem) {
ensureIncluded({ selectorOrElem });
let el;
if (typeof selectorOrElem === 'string') {
this.log(`scroll to ${selectorOrElem}`);
el = this.webdriver.findElement(By.css(selectorOrElem));
} else {
this.log('scroll to element');
el = selectorOrElem;
}
this.webdriver.executeScript('arguments[0].scrollIntoView(true);', el);
return new Promise((resolve) => {
setTimeout(resolve, 250);
});
};
Driver.scrollTo.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Chooses an option from a dropdown based on its human-readable text
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or a WebElement
* @param {string} item - the human-readable text of the option to choose
*/
Driver.chooseSelectItem = async function (selectorOrElem, item) {
ensureIncluded({ selectorOrElem, item });
if (typeof selectorOrElem === 'string') {
this.log(`select item "${item}" from dropdown ${selectorOrElem}`);
} else {
this.log(`select item "${item}" from a dropdown`);
}
// Click the dropdown
(await this._makeSureIsElement(selectorOrElem)).click();
// Get dropdown items
const elements = await this._listSelectElements(selectorOrElem);
// Find the appropriate item
const matchingOptions = [];
for (let i = 0; i < elements.length; i++) {
const isMatch = (item.trim() === (await elements[i].getText()));
if (isMatch) {
matchingOptions.push(elements[i]);
}
}
// Throw errors
if (matchingOptions.length === 0) {
throw new Error(`Could not select item "${item}" from dropdown because it could not be found in the dropdown`);
}
if (matchingOptions.length > 1) {
throw new Error(`Could not select item "${item}" from dropdown because more than one match was found in the dropdown`);
}
// Click the option
return matchingOptions[0].click();
};
Driver.chooseSelectItem.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Chooses an option from a dropdown based on its value
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or the WebElement
* @param {string} value - the value attribute of the option to select
*/
Driver.chooseSelectValue = async function (selectorOrElem, value) {
ensureIncluded({ selectorOrElem, value });
if (typeof selectorOrElem === 'string') {
this.log(`select item with value "${value}" from dropdown ${selectorOrElem}`);
} else {
this.log(`select item with value "${value}" from a dropdown`);
}
// Click the dropdown
(await this._makeSureIsElement(selectorOrElem)).click();
// Get dropdown items
const elements = await this._listSelectElements(selectorOrElem);
// Find the appropriate item
const matchingValues = [];
for (let i = 0; i < elements.length; i++) {
const isMatch = (
value.trim() === (await elements[i].getAttribute('value'))
);
if (isMatch) {
matchingValues.push(elements[i]);
}
}
// Throw errors
if (matchingValues.length === 0) {
throw new Error(`Could not select item with value "${value}" from a dropdown because it could not be found in the dropdown`);
}
if (matchingValues.length > 1) {
throw new Error(`Could not select item with value "${value}" from a dropdown because more than one match was found in the dropdown`);
}
// Click the option
return matchingValues[0].click();
};
Driver.chooseSelectValue.snapshotType = {
before: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
post: SNAPSHOT_TYPES.SCREENSHOT_AND_SOURCE,
};
/**
* Chooses a file in a file chooser
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* file upload field or a WebElement
* @param {string} filePath - the relative path to the file with respect to the
* project directory (the directory from which you started the tests)
*/
Driver.chooseFile = async function (selectorOrElem, filePath) {
ensureIncluded({ selectorOrElem, filePath });
const fileFullPath = path.join(process.env.PWD, filePath);
if (typeof selectorOrElem === 'string') {
this.log(`choose file in chooser "${selectorOrElem}" with path: ${filePath}`);
} else {
this.log(`choose file with path: ${filePath}`);
}
await this.typeInto(selectorOrElem, fileFullPath);
return fileFullPath;
};
Driver.chooseFile.snapshotType = {
during: SNAPSHOT_TYPES.TEXT,
};
/*------------------------------------------------------------------------*/
/* Getting Page Data */
/*------------------------------------------------------------------------*/
/**
* Gets the title of the page
* @return {Promise.<string>} Promise that resolves with the title of page
*/
Driver.getTitle = async function () {
this.log('get title');
const title = await this.webdriver.getTitle();
return title;
};
Driver.getTitle.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/**
* Gets page source
* @return {Promise.<string>} Promise that resolves with the source of the page
*/
Driver.getSource = async function () {
return this.webdriver.executeScript('return document.documentElement.outerHTML');
};
Driver.getSource.snapshotType = {
during: SNAPSHOT_TYPES.TEXT,
};
/**
* Get page JSON (if the body is JSON)
* @return {Promise.<string>} Promise that resolves with the json of the page
*/
Driver.getJSON = async function () {
// Get json contents
// Handle differently for different browsers
let source;
const { browser } = global.dceSeleniumConfig;
if (browser === 'chrome' || browser === 'safari') {
source = await this.webdriver.executeScript('return document.getElementsByTagName("pre")[0].innerHTML');
} else if (browser === 'firefox') {
await this.wait(1000);
await this.webdriver.findElement(By.css('#rawdata-tab')).click();
source = await this.webdriver.executeScript('return document.getElementsByClassName("data")[0].innerHTML');
} else {
source = '{}';
}
try {
const json = JSON.parse(source);
return json;
} catch (err) {
throw new Error(`Page could not be parsed as JSON. Contents: "${source}"`);
}
};
Driver.getJSON.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/**
* Gets the current url of the page
* @return {Promise.<string>} Promise that resolves with the url of the page
*/
Driver.getURL = async function () {
return this.webdriver.executeScript('return window.location.href');
};
Driver.getURL.snapshotType = {
during: SNAPSHOT_TYPES.TEXT,
};
/**
* Gets the query information from the page url
* @return {Promise.<string>} Promise that resolves with the url of the page
*/
Driver.getQuery = async function () {
const url = await this.getURL();
const query = (
url.includes('?')
? url.split('?')[1]
: ''
);
return queryString.parse(query);
};
Driver.getQuery.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/**
* Gets the body of the page
* @return {Promise.<string>} Promise that resolves with the body of the page
*/
Driver.getBody = async function () {
try {
return this.webdriver.executeScript('return document.getElementsByTagName("body")[0].innerHTML');
} catch (err) {
return '';
}
};
Driver.getBody.snapshotType = {
during: SNAPSHOT_TYPES.TEXT,
};
/**
* Gets the list of options in a select dropdown
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or a WebElement
* @return {WebElement[]} the list of web elements
*/
Driver._listSelectElements = async function (selectorOrElem) {
const dropdown = await this._makeSureIsElement(selectorOrElem);
return dropdown.findElements(By.tagName('option'));
};
/**
* Gets the list of dropdown items (the human-readable text)
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or a WebElement
* @return {string[]} the list of human-readable text items in the dropdown
*/
Driver.listSelectItems = async function (selectorOrElem) {
ensureIncluded({ selectorOrElem });
const elements = await this._listSelectElements(selectorOrElem);
const textOptions = [];
for (let i = 0; i < elements.length; i++) {
textOptions.push(await elements[i].getText());
}
return textOptions;
};
Driver.listSelectItems.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/**
* Gets the list of dropdown values (the value attribute of each item)
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* field or a WebElement
* @return {string[]} the list of values of the items in the dropdown
*/
Driver.listSelectValues = async function (selectorOrElem) {
ensureIncluded({ selectorOrElem });
const elements = await this._listSelectElements(selectorOrElem);
const values = [];
for (let i = 0; i < elements.length; i++) {
values.push(await elements[i].getAttribute('value'));
}
return values;
};
Driver.listSelectValues.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/**
* Gets the innerHTML of an element
* @param {string|WebElement} selectorOrElem - the css selector identifying the
* element or a WebElement
* @return {string} the innerHTML of the field
*/
Driver.getElementInnerHTML = async function (selectorOrElem) {
ensureIncluded({ selectorOrElem });
const elem = await this._makeSureIsElement(selectorOrElem);
return elem.getAttribute('innerHTML');
};
Driver.getElementInnerHTML.snapshotType = {
during: SNAPSHOT_TYPES.TEXT,
};
/**
* Searches the page for a <pre id="embedded-metadata">[stringified json]</pre>
* element and parses the stringified json content inside it
* @return {Promise.<string>} Promise that resolves with the json of the
* embedded metadata
*/
Driver.getEmbeddedMetadata = async function () {
return JSON.parse(await this.getElementInnerHTML('#embedded-metadata'));
};
Driver.getEmbeddedMetadata.snapshotType = {
during: SNAPSHOT_TYPES.JSON,
};
/* -------------------------- Existence ------------------------- */
/**
* Checks if an element exists
* @param {string} selector - the css selector identifying the field
* @return {boolean} if true, the element exists
*/
Driver.elementExists = async function (selector) {
ensureIncluded({ selector });
try {
await this.webdriver.findElement(By.css(selector));
return true;
} catch (err) {
if (
err.message.includes('Unable to locate element')
|| err.name === 'NoSuchElementError'
) {
// No element
return false;
}
// Error occurred while checking
throw new Error(`Encountered an error while attempting to check if the element "${selector}" exists: ${err.message}`);
}
};
/**
* Checks if an element does not exist
* @param {string} selector - the css selector identifying the field
* @return {boolean} if true, the element does not exist
*/
Driver.elementAbsent = async function (selector) {
ensureIncluded({ selector });
try {
await this.webdriver.findElement(By.css(selector));
return false;
} catch (err) {
if (
err.message.includes('Unable to locate element')
|| err.name === 'NoSuchElementError'
) {
// No element
return true;
}
// Error occurred while checking
throw new Error(`Encountered an error while attempting to check if the element "${selector}" is absent: ${err.message}`);
}
};
/**
* Checks if an element with specific contents exists
* @param {string} contents - the element contents to search for
* @param {string} [selector] - a css selector to limit the search
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
* @return {boolean} if true, the element exists
*/
Driver.elementWithContentsExists = async function (contents, selector) {
ensureIncluded({ contents });
try {
await this.getElementByContents(contents, selector);
return true;
} catch (err) {
if (
err.message.includes('Unable to locate element')
|| err.name === 'NoSuchElementError'
) {
// No element
return false;
}
// Error occurred while checking
throw new Error(`Encountered an error while attempting to check if the element${selector ? ' "' + selector + '"' : ''} with contents "${contents}" exists: ${err.message}`);
}
};
/**
* Checks if an element with specific contents does not exist
* @param {string} contents - the element contents to search for
* @param {string} [selector] - a css selector to limit the search
*
* Supported css selectors:
* All Elements: null or "*"
* All P Elements: "p"
* All Child Elements of p: "p > *"
* Element By ID: "#foo"
* Element By Class: ".foo"
* Element With Attribute: "*[title]"
* First Child of P: "p > *:first-child"
* Next Element after P: "p + *"
* @return {boolean} if true, the element does not exist
*/
Driver.elementWithContentsAbsent = async function (contents, selector) {
ensureIncluded({ contents });
try {
await this.getElementByContents(contents, selector);
return false;
} catch (err) {
if (
err.message.includes('Unable to locate element')
|| err.name === 'NoSuchElementError'
) {
// No element
return true;
}
// Error occurred while checking
throw new Error(`Encountered an error while attempting to check if the element${selector ? ' "' + selector + '"' : ''} with contents "${contents}" exists: ${err.message}`);
}
};
// Export the driver function
module.exports = Driver;