1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 02:10:12 +01:00
metamask-extension/test/e2e/webdriver/driver.js

285 lines
8.4 KiB
JavaScript
Raw Normal View History

const { promises: fs } = require('fs');
const { strict: assert } = require('assert');
const { until, error: webdriverError, By } = require('selenium-webdriver');
class Driver {
/**
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
* @param {string} browser - The type of browser this driver is controlling
* @param {number} timeout
*/
2020-11-03 00:41:28 +01:00
constructor(driver, browser, extensionUrl, timeout = 10000) {
this.driver = driver;
this.browser = browser;
this.extensionUrl = extensionUrl;
this.timeout = timeout;
}
buildLocator(locator) {
if (typeof locator === 'string') {
// If locator is a string we assume its a css selector
return By.css(locator);
} else if (locator.value) {
// For backwards compatibility, checking if the locator has a value prop
// tells us this is a Selenium locator
return locator;
} else if (locator.xpath) {
// Providing an xpath prop to the object will consume the locator as an
// xpath locator.
return By.xpath(locator.xpath);
} else if (locator.text) {
// Providing a text prop, and optionally a tag, will use xpath to look
// for an element with the tag that has matching text.
return By.xpath(
`//${locator.tag ?? '*'}[contains(text(), '${locator.text}')]`,
);
}
throw new Error(
`The locator '${locator}' is not supported by the E2E test driver`,
);
}
2020-11-03 00:41:28 +01:00
async delay(time) {
await new Promise((resolve) => setTimeout(resolve, time));
}
2020-11-03 00:41:28 +01:00
async wait(condition, timeout = this.timeout) {
await this.driver.wait(condition, timeout);
}
2020-11-03 00:41:28 +01:00
async quit() {
await this.driver.quit();
}
// Element interactions
async findElement(rawLocator) {
const locator = this.buildLocator(rawLocator);
return await this.driver.wait(until.elementLocated(locator), this.timeout);
}
async findVisibleElement(rawLocator) {
const locator = this.buildLocator(rawLocator);
const element = await this.findElement(locator);
await this.driver.wait(until.elementIsVisible(element), this.timeout);
return element;
}
async findClickableElement(rawLocator) {
const locator = this.buildLocator(rawLocator);
const element = await this.findElement(locator);
await Promise.all([
this.driver.wait(until.elementIsVisible(element), this.timeout),
this.driver.wait(until.elementIsEnabled(element), this.timeout),
]);
return element;
}
async findElements(rawLocator) {
const locator = this.buildLocator(rawLocator);
return await this.driver.wait(until.elementsLocated(locator), this.timeout);
}
async findClickableElements(rawLocator) {
const locator = this.buildLocator(rawLocator);
const elements = await this.findElements(locator);
2020-11-03 00:41:28 +01:00
await Promise.all(
elements.reduce((acc, element) => {
acc.push(
this.driver.wait(until.elementIsVisible(element), this.timeout),
this.driver.wait(until.elementIsEnabled(element), this.timeout),
);
return acc;
2020-11-03 00:41:28 +01:00
}, []),
);
return elements;
}
async clickElement(rawLocator) {
const locator = this.buildLocator(rawLocator);
const element = await this.findClickableElement(locator);
await element.click();
}
async clickPoint(rawLocator, x, y) {
const locator = this.buildLocator(rawLocator);
const element = await this.findElement(locator);
await this.driver
.actions()
.move({ origin: element, x, y })
.click()
.perform();
}
2020-11-03 00:41:28 +01:00
async scrollToElement(element) {
await this.driver.executeScript(
'arguments[0].scrollIntoView(true)',
element,
);
}
async assertElementNotPresent(rawLocator) {
const locator = this.buildLocator(rawLocator);
let dataTab;
try {
dataTab = await this.findElement(locator);
} catch (err) {
2020-11-03 00:41:28 +01:00
assert(
err instanceof webdriverError.NoSuchElementError ||
err instanceof webdriverError.TimeoutError,
);
}
assert.ok(!dataTab, 'Found element that should not be present');
}
// Navigation
2020-11-03 00:41:28 +01:00
async navigate(page = Driver.PAGES.HOME) {
return await this.driver.get(`${this.extensionUrl}/${page}.html`);
}
// Metrics
2020-11-03 00:41:28 +01:00
async collectMetrics() {
return await this.driver.executeScript(collectMetrics);
}
// Window management
2020-11-03 00:41:28 +01:00
async openNewPage(url) {
const newHandle = await this.driver.switchTo().newWindow();
await this.driver.get(url);
return newHandle;
}
2020-11-03 00:41:28 +01:00
async switchToWindow(handle) {
await this.driver.switchTo().window(handle);
}
2020-11-03 00:41:28 +01:00
async getAllWindowHandles() {
return await this.driver.getAllWindowHandles();
}
2020-11-03 00:41:28 +01:00
async waitUntilXWindowHandles(x, delayStep = 1000, timeout = 5000) {
let timeElapsed = 0;
let windowHandles = [];
while (timeElapsed <= timeout) {
windowHandles = await this.driver.getAllWindowHandles();
if (windowHandles.length === x) {
return windowHandles;
}
await this.delay(delayStep);
timeElapsed += delayStep;
}
throw new Error('waitUntilXWindowHandles timed out polling window handles');
}
2020-11-03 00:41:28 +01:00
async switchToWindowWithTitle(title, windowHandles) {
// eslint-disable-next-line no-param-reassign
windowHandles = windowHandles || (await this.driver.getAllWindowHandles());
for (const handle of windowHandles) {
await this.driver.switchTo().window(handle);
const handleTitle = await this.driver.getTitle();
if (handleTitle === title) {
return handle;
}
}
throw new Error(`No window with title: ${title}`);
}
/**
* Closes all windows except those in the given list of exceptions
* @param {Array<string>} exceptions - The list of window handle exceptions
* @param {Array} [windowHandles] - The full list of window handles
* @returns {Promise<void>}
*/
2020-11-03 00:41:28 +01:00
async closeAllWindowHandlesExcept(exceptions, windowHandles) {
// eslint-disable-next-line no-param-reassign
windowHandles = windowHandles || (await this.driver.getAllWindowHandles());
for (const handle of windowHandles) {
if (!exceptions.includes(handle)) {
await this.driver.switchTo().window(handle);
await this.delay(1000);
await this.driver.close();
await this.delay(1000);
}
}
}
// Error handling
2020-11-03 00:41:28 +01:00
async verboseReportOnFailure(title) {
const artifactDir = `./test-artifacts/${this.browser}/${title}`;
const filepathBase = `${artifactDir}/test-failure`;
await fs.mkdir(artifactDir, { recursive: true });
const screenshot = await this.driver.takeScreenshot();
2020-11-03 00:41:28 +01:00
await fs.writeFile(`${filepathBase}-screenshot.png`, screenshot, {
encoding: 'base64',
});
const htmlSource = await this.driver.getPageSource();
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource);
const uiState = await this.driver.executeScript(
() => window.getCleanAppState && window.getCleanAppState(),
);
2020-11-03 00:41:28 +01:00
await fs.writeFile(
`${filepathBase}-state.json`,
JSON.stringify(uiState, null, 2),
);
}
2020-11-03 00:41:28 +01:00
async checkBrowserForConsoleErrors() {
const ignoredLogTypes = ['WARNING'];
const ignoredErrorMessages = [
// Third-party Favicon 404s show up as errors
'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)',
];
const browserLogs = await this.driver.manage().logs().get('browser');
2020-11-03 00:41:28 +01:00
const errorEntries = browserLogs.filter(
(entry) => !ignoredLogTypes.includes(entry.level.toString()),
);
const errorObjects = errorEntries.map((entry) => entry.toJSON());
2020-11-03 00:41:28 +01:00
return errorObjects.filter(
(entry) =>
!ignoredErrorMessages.some((message) =>
entry.message.includes(message),
),
);
}
}
2020-11-03 00:41:28 +01:00
function collectMetrics() {
const results = {
paint: {},
navigation: [],
};
window.performance.getEntriesByType('paint').forEach((paintEntry) => {
results.paint[paintEntry.name] = paintEntry.startTime;
});
2020-11-03 00:41:28 +01:00
window.performance
.getEntriesByType('navigation')
.forEach((navigationEntry) => {
results.navigation.push({
domContentLoaded: navigationEntry.domContentLoadedEventEnd,
load: navigationEntry.loadEventEnd,
domInteractive: navigationEntry.domInteractive,
redirectCount: navigationEntry.redirectCount,
type: navigationEntry.type,
});
});
return results;
}
Driver.PAGES = {
HOME: 'home',
NOTIFICATION: 'notification',
POPUP: 'popup',
};
module.exports = Driver;