2021-02-04 19:15:23 +01:00
|
|
|
const { promises: fs } = require('fs');
|
|
|
|
const { strict: assert } = require('assert');
|
2022-11-21 20:04:03 +01:00
|
|
|
const {
|
|
|
|
By,
|
|
|
|
Condition,
|
|
|
|
error: webdriverError,
|
|
|
|
Key,
|
|
|
|
until,
|
|
|
|
} = require('selenium-webdriver');
|
2021-04-08 17:41:23 +02:00
|
|
|
const cssToXPath = require('css-to-xpath');
|
2020-01-13 16:07:32 +01:00
|
|
|
|
2021-04-12 17:32:38 +02:00
|
|
|
/**
|
|
|
|
* Temporary workaround to patch selenium's element handle API with methods
|
|
|
|
* that match the playwright API for Elements
|
2022-01-07 16:57:33 +01:00
|
|
|
*
|
2022-07-27 15:28:05 +02:00
|
|
|
* @param {object} element - Selenium Element
|
2022-01-07 16:57:33 +01:00
|
|
|
* @param driver
|
2022-07-27 15:28:05 +02:00
|
|
|
* @returns {object} modified Selenium Element
|
2021-04-12 17:32:38 +02:00
|
|
|
*/
|
2021-04-13 23:36:02 +02:00
|
|
|
function wrapElementWithAPI(element, driver) {
|
2021-04-12 17:32:38 +02:00
|
|
|
element.press = (key) => element.sendKeys(key);
|
|
|
|
element.fill = async (input) => {
|
|
|
|
// The 'fill' method in playwright replaces existing input
|
2022-05-05 19:51:57 +02:00
|
|
|
await element.sendKeys(
|
|
|
|
Key.chord(driver.Key.MODIFIER, 'a', driver.Key.BACK_SPACE),
|
|
|
|
);
|
2021-04-12 17:32:38 +02:00
|
|
|
await element.sendKeys(input);
|
|
|
|
};
|
2021-04-13 23:36:02 +02:00
|
|
|
element.waitForElementState = async (state, timeout) => {
|
|
|
|
switch (state) {
|
|
|
|
case 'hidden':
|
|
|
|
return await driver.wait(until.stalenessOf(element), timeout);
|
|
|
|
case 'visible':
|
|
|
|
return await driver.wait(until.elementIsVisible(element), timeout);
|
|
|
|
default:
|
|
|
|
throw new Error(`Provided state: '${state}' is not supported`);
|
|
|
|
}
|
|
|
|
};
|
2021-04-12 17:32:38 +02:00
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
2022-11-21 20:04:03 +01:00
|
|
|
until.elementIsNotPresent = function elementIsNotPresent(locator) {
|
|
|
|
return new Condition(`Element not present`, function (driver) {
|
|
|
|
return driver.findElements(By.css(locator)).then(function (elements) {
|
|
|
|
return elements.length === 0;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-11-11 21:26:49 +01:00
|
|
|
/**
|
|
|
|
* For Selenium WebDriver API documentation, see:
|
|
|
|
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
|
|
|
|
*/
|
2020-01-13 16:07:32 +01:00
|
|
|
class Driver {
|
|
|
|
/**
|
|
|
|
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
|
|
|
|
* @param {string} browser - The type of browser this driver is controlling
|
2022-01-07 16:57:33 +01:00
|
|
|
* @param extensionUrl
|
2020-01-13 16:07:32 +01:00
|
|
|
* @param {number} timeout
|
|
|
|
*/
|
2023-07-12 20:27:44 +02:00
|
|
|
constructor(driver, browser, extensionUrl, timeout = 10 * 1000) {
|
2021-02-04 19:15:23 +01:00
|
|
|
this.driver = driver;
|
|
|
|
this.browser = browser;
|
|
|
|
this.extensionUrl = extensionUrl;
|
|
|
|
this.timeout = timeout;
|
2022-11-14 15:35:08 +01:00
|
|
|
this.exceptions = [];
|
2023-02-23 15:27:36 +01:00
|
|
|
this.errors = [];
|
2021-04-12 17:32:38 +02:00
|
|
|
// The following values are found in
|
|
|
|
// https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110
|
|
|
|
// These should be replaced with string constants 'Enter' etc for playwright.
|
|
|
|
this.Key = {
|
|
|
|
BACK_SPACE: '\uE003',
|
|
|
|
ENTER: '\uE007',
|
2022-04-27 20:21:40 +02:00
|
|
|
SPACE: '\uE00D',
|
2022-05-05 19:51:57 +02:00
|
|
|
CONTROL: '\uE009',
|
|
|
|
COMMAND: '\uE03D',
|
|
|
|
MODIFIER: process.platform === 'darwin' ? Key.COMMAND : Key.CONTROL,
|
2021-04-12 17:32:38 +02:00
|
|
|
};
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-11-11 21:26:49 +01:00
|
|
|
async executeAsyncScript(script, ...args) {
|
|
|
|
return this.driver.executeAsyncScript(script, args);
|
|
|
|
}
|
|
|
|
|
2021-11-03 01:01:01 +01:00
|
|
|
async executeScript(script, ...args) {
|
|
|
|
return this.driver.executeScript(script, args);
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
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) {
|
2021-04-08 17:41:23 +02:00
|
|
|
// Providing a text prop, and optionally a tag or css prop, will use
|
|
|
|
// xpath to look for an element with the tag that has matching text.
|
|
|
|
if (locator.css) {
|
|
|
|
// When providing css prop we use cssToXPath to build a xpath string
|
|
|
|
// We provide two cases to check for, first a text node of the
|
|
|
|
// element that matches the text provided OR we test the stringified
|
|
|
|
// contents of the element in the case where text is split across
|
|
|
|
// multiple children. In the later case non literal spaces are stripped
|
|
|
|
// so we do the same with the input to provide a consistent API.
|
|
|
|
const xpath = cssToXPath
|
|
|
|
.parse(locator.css)
|
|
|
|
.where(
|
|
|
|
cssToXPath.xPathBuilder
|
|
|
|
.string()
|
|
|
|
.contains(locator.text)
|
|
|
|
.or(
|
|
|
|
cssToXPath.xPathBuilder
|
|
|
|
.string()
|
|
|
|
.contains(locator.text.split(' ').join('')),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toXPath();
|
|
|
|
return By.xpath(xpath);
|
|
|
|
}
|
|
|
|
// The tag prop is optional and further refines which elements match
|
2021-04-02 21:45:11 +02:00
|
|
|
return By.xpath(
|
|
|
|
`//${locator.tag ?? '*'}[contains(text(), '${locator.text}')]`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
throw new Error(
|
|
|
|
`The locator '${locator}' is not supported by the E2E test driver`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-12 17:32:38 +02:00
|
|
|
async fill(rawLocator, input) {
|
|
|
|
const element = await this.findElement(rawLocator);
|
|
|
|
await element.fill(input);
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
|
|
|
async press(rawLocator, keys) {
|
|
|
|
const element = await this.findElement(rawLocator);
|
|
|
|
await element.press(keys);
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async delay(time) {
|
2021-02-04 19:15:23 +01:00
|
|
|
await new Promise((resolve) => setTimeout(resolve, time));
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async wait(condition, timeout = this.timeout) {
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.driver.wait(condition, timeout);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-04-08 17:41:23 +02:00
|
|
|
async waitForSelector(
|
|
|
|
rawLocator,
|
|
|
|
{ timeout = this.timeout, state = 'visible' } = {},
|
|
|
|
) {
|
|
|
|
// Playwright has a waitForSelector method that will become a shallow
|
|
|
|
// replacement for the implementation below. It takes an option options
|
|
|
|
// bucket that can include the state attribute to wait for elements that
|
|
|
|
// match the selector to be removed from the DOM.
|
2021-04-12 17:32:38 +02:00
|
|
|
let element;
|
|
|
|
if (!['visible', 'detached'].includes(state)) {
|
|
|
|
throw new Error(`Provided state selector ${state} is not supported`);
|
|
|
|
}
|
2021-04-08 17:41:23 +02:00
|
|
|
if (state === 'visible') {
|
2023-07-10 21:39:37 +02:00
|
|
|
element = await this.driver.wait(
|
|
|
|
until.elementLocated(this.buildLocator(rawLocator)),
|
|
|
|
timeout,
|
|
|
|
);
|
2021-04-08 17:41:23 +02:00
|
|
|
} else if (state === 'detached') {
|
2021-04-12 17:32:38 +02:00
|
|
|
element = await this.driver.wait(
|
2023-07-10 21:39:37 +02:00
|
|
|
until.stalenessOf(await this.findElement(rawLocator)),
|
2021-04-08 17:41:23 +02:00
|
|
|
timeout,
|
|
|
|
);
|
|
|
|
}
|
2021-04-13 23:36:02 +02:00
|
|
|
return wrapElementWithAPI(element, this);
|
2021-04-08 17:41:23 +02:00
|
|
|
}
|
|
|
|
|
2022-10-27 14:16:50 +02:00
|
|
|
async waitForNonEmptyElement(element) {
|
|
|
|
await this.driver.wait(async () => {
|
|
|
|
const elemText = await element.getText();
|
|
|
|
const empty = elemText === '';
|
|
|
|
return !empty;
|
|
|
|
}, this.timeout);
|
|
|
|
}
|
|
|
|
|
2022-11-21 20:04:03 +01:00
|
|
|
async waitForElementNotPresent(element) {
|
|
|
|
return await this.driver.wait(until.elementIsNotPresent(element));
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async quit() {
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.driver.quit();
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Element interactions
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async findElement(rawLocator) {
|
|
|
|
const locator = this.buildLocator(rawLocator);
|
2021-04-12 17:32:38 +02:00
|
|
|
const element = await this.driver.wait(
|
|
|
|
until.elementLocated(locator),
|
|
|
|
this.timeout,
|
|
|
|
);
|
2021-04-13 23:36:02 +02:00
|
|
|
return wrapElementWithAPI(element, this);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async findVisibleElement(rawLocator) {
|
2023-07-10 21:39:37 +02:00
|
|
|
const element = await this.findElement(rawLocator);
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.driver.wait(until.elementIsVisible(element), this.timeout);
|
2021-04-13 23:36:02 +02:00
|
|
|
return wrapElementWithAPI(element, this);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async findClickableElement(rawLocator) {
|
2023-07-10 21:39:37 +02:00
|
|
|
const element = await this.findElement(rawLocator);
|
2020-01-15 20:34:15 +01:00
|
|
|
await Promise.all([
|
|
|
|
this.driver.wait(until.elementIsVisible(element), this.timeout),
|
|
|
|
this.driver.wait(until.elementIsEnabled(element), this.timeout),
|
2021-02-04 19:15:23 +01:00
|
|
|
]);
|
2021-04-13 23:36:02 +02:00
|
|
|
return wrapElementWithAPI(element, this);
|
2020-01-15 20:34:15 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async findElements(rawLocator) {
|
|
|
|
const locator = this.buildLocator(rawLocator);
|
2021-04-12 17:32:38 +02:00
|
|
|
const elements = await this.driver.wait(
|
|
|
|
until.elementsLocated(locator),
|
|
|
|
this.timeout,
|
|
|
|
);
|
2021-04-13 23:36:02 +02:00
|
|
|
return elements.map((element) => wrapElementWithAPI(element, this));
|
2020-01-15 20:34:15 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async findClickableElements(rawLocator) {
|
2023-07-10 21:39:37 +02:00
|
|
|
const elements = await this.findElements(rawLocator);
|
2020-11-03 00:41:28 +01:00
|
|
|
await Promise.all(
|
|
|
|
elements.reduce((acc, element) => {
|
2020-01-15 20:34:15 +01:00
|
|
|
acc.push(
|
|
|
|
this.driver.wait(until.elementIsVisible(element), this.timeout),
|
|
|
|
this.driver.wait(until.elementIsEnabled(element), this.timeout),
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
|
|
|
return acc;
|
2020-11-03 00:41:28 +01:00
|
|
|
}, []),
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
2021-04-13 23:36:02 +02:00
|
|
|
return elements.map((element) => wrapElementWithAPI(element, this));
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async clickElement(rawLocator) {
|
2023-07-10 21:39:37 +02:00
|
|
|
const element = await this.findClickableElement(rawLocator);
|
2021-02-04 19:15:23 +01:00
|
|
|
await element.click();
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2023-07-21 23:32:51 +02:00
|
|
|
async clickElementSafe(rawLocator) {
|
|
|
|
// for instances where an element such as a scroll button does not
|
|
|
|
// show up because of render differences, proceed to the next step
|
|
|
|
// without causing a test failure, but provide a console log of why.
|
|
|
|
try {
|
|
|
|
const element = await this.findClickableElement(rawLocator);
|
|
|
|
await element.click();
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Element ${rawLocator} not found (${e})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async clickPoint(rawLocator, x, y) {
|
2023-07-10 21:39:37 +02:00
|
|
|
const element = await this.findElement(rawLocator);
|
2020-05-27 17:31:53 +02:00
|
|
|
await this.driver
|
|
|
|
.actions()
|
|
|
|
.move({ origin: element, x, y })
|
|
|
|
.click()
|
2021-02-04 19:15:23 +01:00
|
|
|
.perform();
|
2020-05-27 17:31:53 +02:00
|
|
|
}
|
|
|
|
|
2023-06-20 20:27:10 +02:00
|
|
|
async holdMouseDownOnElement(rawLocator, ms) {
|
|
|
|
const locator = this.buildLocator(rawLocator);
|
|
|
|
const element = await this.findClickableElement(locator);
|
|
|
|
await this.driver
|
|
|
|
.actions()
|
|
|
|
.move({ origin: element, x: 1, y: 1 })
|
|
|
|
.press()
|
|
|
|
.pause(ms)
|
|
|
|
.release()
|
|
|
|
.perform();
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async scrollToElement(element) {
|
|
|
|
await this.driver.executeScript(
|
|
|
|
'arguments[0].scrollIntoView(true)',
|
|
|
|
element,
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:45:11 +02:00
|
|
|
async assertElementNotPresent(rawLocator) {
|
2021-02-04 19:15:23 +01:00
|
|
|
let dataTab;
|
2020-01-13 16:07:32 +01:00
|
|
|
try {
|
2023-07-10 21:39:37 +02:00
|
|
|
dataTab = await this.findElement(rawLocator);
|
2020-01-13 16:07:32 +01:00
|
|
|
} catch (err) {
|
2020-11-03 00:41:28 +01:00
|
|
|
assert(
|
|
|
|
err instanceof webdriverError.NoSuchElementError ||
|
|
|
|
err instanceof webdriverError.TimeoutError,
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
2021-02-04 19:15:23 +01:00
|
|
|
assert.ok(!dataTab, 'Found element that should not be present');
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2022-09-27 17:51:46 +02:00
|
|
|
async isElementPresent(rawLocator) {
|
2022-03-09 09:35:14 +01:00
|
|
|
try {
|
2022-09-27 17:51:46 +02:00
|
|
|
await this.findElement(rawLocator);
|
2022-03-09 09:35:14 +01:00
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-15 16:58:39 +01:00
|
|
|
async isElementPresentAndVisible(rawLocator) {
|
|
|
|
try {
|
|
|
|
await this.findVisibleElement(rawLocator);
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 22:43:21 +01:00
|
|
|
/**
|
|
|
|
* Paste a string into a field.
|
|
|
|
*
|
2022-07-01 18:40:31 +02:00
|
|
|
* @param {string} rawLocator - The element locator.
|
2022-03-16 22:43:21 +01:00
|
|
|
* @param {string} contentToPaste - The content to paste.
|
|
|
|
*/
|
2022-07-01 18:40:31 +02:00
|
|
|
async pasteIntoField(rawLocator, contentToPaste) {
|
2022-03-16 22:43:21 +01:00
|
|
|
// Throw if double-quote is present in content to paste
|
|
|
|
// so that we don't have to worry about escaping double-quotes
|
|
|
|
if (contentToPaste.includes('"')) {
|
|
|
|
throw new Error('Cannot paste content with double-quote');
|
|
|
|
}
|
|
|
|
// Click to focus the field
|
2022-07-01 18:40:31 +02:00
|
|
|
await this.clickElement(rawLocator);
|
2022-03-16 22:43:21 +01:00
|
|
|
await this.executeScript(
|
|
|
|
`navigator.clipboard.writeText("${contentToPaste}")`,
|
|
|
|
);
|
2022-07-01 18:40:31 +02:00
|
|
|
await this.fill(rawLocator, Key.chord(this.Key.MODIFIER, 'v'));
|
2022-03-16 22:43:21 +01:00
|
|
|
}
|
|
|
|
|
2020-01-20 18:03:07 +01:00
|
|
|
// Navigation
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async navigate(page = Driver.PAGES.HOME) {
|
2021-02-04 19:15:23 +01:00
|
|
|
return await this.driver.get(`${this.extensionUrl}/${page}.html`);
|
2020-01-20 18:03:07 +01:00
|
|
|
}
|
|
|
|
|
2022-09-22 09:39:34 +02:00
|
|
|
async getCurrentUrl() {
|
|
|
|
return await this.driver.getCurrentUrl();
|
|
|
|
}
|
|
|
|
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
// Metrics
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async collectMetrics() {
|
2021-02-04 19:15:23 +01:00
|
|
|
return await this.driver.executeScript(collectMetrics);
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
}
|
|
|
|
|
2020-01-13 16:07:32 +01:00
|
|
|
// Window management
|
2023-05-04 21:58:39 +02:00
|
|
|
async openNewURL(url) {
|
|
|
|
await this.driver.get(url);
|
|
|
|
}
|
2020-01-13 16:07:32 +01:00
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async openNewPage(url) {
|
2021-02-04 19:15:23 +01:00
|
|
|
const newHandle = await this.driver.switchTo().newWindow();
|
2023-05-04 21:58:39 +02:00
|
|
|
await this.openNewURL(url);
|
2021-02-04 19:15:23 +01:00
|
|
|
return newHandle;
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async switchToWindow(handle) {
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.driver.switchTo().window(handle);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2023-05-04 21:58:39 +02:00
|
|
|
async switchToNewWindow() {
|
|
|
|
await this.driver.switchTo().newWindow('window');
|
|
|
|
}
|
|
|
|
|
2022-05-06 00:28:48 +02:00
|
|
|
async switchToFrame(element) {
|
|
|
|
await this.driver.switchTo().frame(element);
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async getAllWindowHandles() {
|
2021-02-04 19:15:23 +01:00
|
|
|
return await this.driver.getAllWindowHandles();
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2023-07-11 21:17:27 +02:00
|
|
|
async waitUntilXWindowHandles(x, delayStep = 1000, timeout = this.timeout) {
|
2021-02-04 19:15:23 +01:00
|
|
|
let timeElapsed = 0;
|
|
|
|
let windowHandles = [];
|
2020-01-13 16:07:32 +01:00
|
|
|
while (timeElapsed <= timeout) {
|
2021-02-04 19:15:23 +01:00
|
|
|
windowHandles = await this.driver.getAllWindowHandles();
|
2023-07-12 20:27:44 +02:00
|
|
|
|
2020-01-13 16:07:32 +01:00
|
|
|
if (windowHandles.length === x) {
|
2021-02-04 19:15:23 +01:00
|
|
|
return windowHandles;
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.delay(delayStep);
|
|
|
|
timeElapsed += delayStep;
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
2021-02-04 19:15:23 +01:00
|
|
|
throw new Error('waitUntilXWindowHandles timed out polling window handles');
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-09-17 20:27:09 +02:00
|
|
|
async switchToWindowWithTitle(
|
|
|
|
title,
|
2021-09-29 00:40:07 +02:00
|
|
|
initialWindowHandles,
|
2021-09-17 20:27:09 +02:00
|
|
|
delayStep = 1000,
|
2023-07-12 20:27:44 +02:00
|
|
|
timeout = this.timeout,
|
2021-09-17 20:27:09 +02:00
|
|
|
) {
|
2021-09-29 00:40:07 +02:00
|
|
|
let windowHandles =
|
|
|
|
initialWindowHandles || (await this.driver.getAllWindowHandles());
|
2021-09-17 20:27:09 +02:00
|
|
|
let timeElapsed = 0;
|
|
|
|
while (timeElapsed <= timeout) {
|
|
|
|
for (const handle of windowHandles) {
|
|
|
|
await this.driver.switchTo().window(handle);
|
2023-07-12 20:27:44 +02:00
|
|
|
|
2021-09-17 20:27:09 +02:00
|
|
|
const handleTitle = await this.driver.getTitle();
|
|
|
|
if (handleTitle === title) {
|
|
|
|
return handle;
|
|
|
|
}
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
2021-09-17 20:27:09 +02:00
|
|
|
await this.delay(delayStep);
|
|
|
|
timeElapsed += delayStep;
|
2021-09-29 00:40:07 +02:00
|
|
|
// refresh the window handles
|
|
|
|
windowHandles = await this.driver.getAllWindowHandles();
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 19:15:23 +01:00
|
|
|
throw new Error(`No window with title: ${title}`);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2023-05-19 12:17:53 +02:00
|
|
|
async closeWindow() {
|
|
|
|
await this.driver.close();
|
|
|
|
}
|
|
|
|
|
2023-06-20 12:17:08 +02:00
|
|
|
async closeWindowHandle(windowHandle) {
|
|
|
|
await this.driver.switchTo().window(windowHandle);
|
|
|
|
await this.driver.close();
|
|
|
|
}
|
|
|
|
|
2022-11-30 14:12:09 +01:00
|
|
|
// Close Alert Popup
|
|
|
|
async closeAlertPopup() {
|
|
|
|
return await this.driver.switchTo().alert().accept();
|
|
|
|
}
|
|
|
|
|
2020-01-13 16:07:32 +01:00
|
|
|
/**
|
|
|
|
* Closes all windows except those in the given list of exceptions
|
2022-01-07 16:57:33 +01:00
|
|
|
*
|
2020-01-13 19:36:36 +01:00
|
|
|
* @param {Array<string>} exceptions - The list of window handle exceptions
|
|
|
|
* @param {Array} [windowHandles] - The full list of window handles
|
2020-01-13 16:07:32 +01:00
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2020-11-03 00:41:28 +01:00
|
|
|
async closeAllWindowHandlesExcept(exceptions, windowHandles) {
|
2020-08-15 13:58:11 +02:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
2021-02-04 19:15:23 +01:00
|
|
|
windowHandles = windowHandles || (await this.driver.getAllWindowHandles());
|
2020-01-13 16:07:32 +01:00
|
|
|
|
|
|
|
for (const handle of windowHandles) {
|
|
|
|
if (!exceptions.includes(handle)) {
|
2021-02-04 19:15:23 +01:00
|
|
|
await this.driver.switchTo().window(handle);
|
|
|
|
await this.delay(1000);
|
|
|
|
await this.driver.close();
|
|
|
|
await this.delay(1000);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error handling
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
async verboseReportOnFailure(title) {
|
2021-02-04 19:15:23 +01:00
|
|
|
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',
|
2021-02-04 19:15:23 +01:00
|
|
|
});
|
|
|
|
const htmlSource = await this.driver.getPageSource();
|
|
|
|
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource);
|
2020-08-10 17:58:17 +02:00
|
|
|
const uiState = await this.driver.executeScript(
|
2022-11-09 20:28:32 +01:00
|
|
|
() =>
|
2022-11-17 15:59:06 +01:00
|
|
|
window.stateHooks?.getCleanAppState &&
|
2022-11-09 20:28:32 +01:00
|
|
|
window.stateHooks.getCleanAppState(),
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
2020-11-03 00:41:28 +01:00
|
|
|
await fs.writeFile(
|
|
|
|
`${filepathBase}-state.json`,
|
|
|
|
JSON.stringify(uiState, null, 2),
|
2021-02-04 19:15:23 +01:00
|
|
|
);
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
|
2022-07-20 01:07:15 +02:00
|
|
|
async checkBrowserForLavamoatLogs() {
|
|
|
|
const browserLogs = (
|
|
|
|
await fs.readFile(
|
|
|
|
`${process.cwd()}/test-artifacts/chrome/chrome_debug.log`,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.toString('utf-8')
|
|
|
|
.split(/\r?\n/u);
|
|
|
|
|
|
|
|
await fs.writeFile('/tmp/all_logs.json', JSON.stringify(browserLogs));
|
|
|
|
|
|
|
|
return browserLogs;
|
|
|
|
}
|
|
|
|
|
2023-02-23 15:27:36 +01:00
|
|
|
async checkBrowserForExceptions(failOnConsoleError) {
|
2022-11-14 15:35:08 +01:00
|
|
|
const { exceptions } = this;
|
|
|
|
const cdpConnection = await this.driver.createCDPConnection('page');
|
2023-02-23 15:27:36 +01:00
|
|
|
await this.driver.onLogException(cdpConnection, (exception) => {
|
2022-11-14 15:35:08 +01:00
|
|
|
const { description } = exception.exceptionDetails.exception;
|
|
|
|
exceptions.push(description);
|
2023-02-23 15:27:36 +01:00
|
|
|
logBrowserError(failOnConsoleError, description);
|
2022-11-14 15:35:08 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-02-23 15:27:36 +01:00
|
|
|
async checkBrowserForConsoleErrors(failOnConsoleError) {
|
2020-01-13 16:07:32 +01:00
|
|
|
const ignoredErrorMessages = [
|
|
|
|
// Third-party Favicon 404s show up as errors
|
2022-03-10 23:01:55 +01:00
|
|
|
'favicon.ico - Failed to load resource: the server responded with a status of 404',
|
2021-11-25 12:47:33 +01:00
|
|
|
// Sentry rate limiting
|
|
|
|
'Failed to load resource: the server responded with a status of 429',
|
2022-01-04 19:10:59 +01:00
|
|
|
// 4Byte
|
|
|
|
'Failed to load resource: the server responded with a status of 502 (Bad Gateway)',
|
2021-02-04 19:15:23 +01:00
|
|
|
];
|
2023-03-31 15:22:33 +02:00
|
|
|
|
2023-02-23 15:27:36 +01:00
|
|
|
const { errors } = this;
|
|
|
|
const cdpConnection = await this.driver.createCDPConnection('page');
|
|
|
|
await this.driver.onLogEvent(cdpConnection, (event) => {
|
|
|
|
if (event.type === 'error') {
|
2023-03-31 15:22:33 +02:00
|
|
|
const eventDescriptions = event.args.filter(
|
2023-02-23 15:27:36 +01:00
|
|
|
(err) => err.description !== undefined,
|
|
|
|
);
|
2023-03-31 15:22:33 +02:00
|
|
|
|
|
|
|
const [eventDescription] = eventDescriptions;
|
2023-02-23 15:27:36 +01:00
|
|
|
const ignore = ignoredErrorMessages.some((message) =>
|
2023-03-31 15:22:33 +02:00
|
|
|
eventDescription?.description.includes(message),
|
2023-02-23 15:27:36 +01:00
|
|
|
);
|
|
|
|
if (!ignore) {
|
2023-03-31 15:22:33 +02:00
|
|
|
errors.push(eventDescription?.description);
|
|
|
|
logBrowserError(failOnConsoleError, eventDescription?.description);
|
2023-02-23 15:27:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function logBrowserError(failOnConsoleError, errorMessage) {
|
|
|
|
if (failOnConsoleError) {
|
|
|
|
throw new Error(errorMessage);
|
|
|
|
} else {
|
|
|
|
console.error(new Error(errorMessage));
|
2020-01-13 16:07:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:41:28 +01:00
|
|
|
function collectMetrics() {
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
const results = {
|
|
|
|
paint: {},
|
|
|
|
navigation: [],
|
2021-02-04 19:15:23 +01:00
|
|
|
};
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
|
2020-04-15 19:23:27 +02:00
|
|
|
window.performance.getEntriesByType('paint').forEach((paintEntry) => {
|
2021-02-04 19:15:23 +01:00
|
|
|
results.paint[paintEntry.name] = paintEntry.startTime;
|
|
|
|
});
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
|
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,
|
2021-02-04 19:15:23 +01:00
|
|
|
});
|
|
|
|
});
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
|
2021-02-04 19:15:23 +01:00
|
|
|
return results;
|
Add benchmark script (#7869)
The script `benchmark.js` will collect page load metrics from the
extension, and print them to a file or the console. A method for
collecting metrics was added to the web driver to help with this.
This script will calculate the min, max, average, and standard
deviation for four metrics: 'firstPaint', 'domContentLoaded', 'load',
and 'domInteractive'. The variation between samples is sometimes high,
with the results varying between samples if only 3 were taken. However,
all tests I've done locally with 5 samples have produced results within
one standard deviation of each other. The default number of samples has
been set to 10, which should be more than enough to produce consistent
results.
The benchmark can be run with the npm script `benchmark:chrome` or
`benchmark:firefox`, e.g. `yarn benchmark:chrome`.
2020-01-21 17:02:45 +01:00
|
|
|
}
|
|
|
|
|
2020-01-20 18:03:07 +01:00
|
|
|
Driver.PAGES = {
|
2021-11-03 01:01:01 +01:00
|
|
|
BACKGROUND: 'background',
|
2020-01-20 18:03:07 +01:00
|
|
|
HOME: 'home',
|
|
|
|
NOTIFICATION: 'notification',
|
|
|
|
POPUP: 'popup',
|
2021-02-04 19:15:23 +01:00
|
|
|
};
|
2020-01-20 18:03:07 +01:00
|
|
|
|
2021-02-04 19:15:23 +01:00
|
|
|
module.exports = Driver;
|