const { promises: fs } = require('fs') const { strict: assert } = require('assert') const { until, error: webdriverError } = 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 */ constructor(driver, browser, extensionUrl, timeout = 10000) { this.driver = driver this.browser = browser this.extensionUrl = extensionUrl this.timeout = timeout } async delay(time) { await new Promise((resolve) => setTimeout(resolve, time)) } async wait(condition, timeout = this.timeout) { await this.driver.wait(condition, timeout) } async quit() { await this.driver.quit() } // Element interactions async findElement(locator) { return await this.driver.wait(until.elementLocated(locator), this.timeout) } async findVisibleElement(locator) { const element = await this.findElement(locator) await this.driver.wait(until.elementIsVisible(element), this.timeout) return element } async findClickableElement(locator) { 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(locator) { return await this.driver.wait(until.elementsLocated(locator), this.timeout) } async findClickableElements(locator) { const elements = await this.findElements(locator) 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 }, []), ) return elements } async clickElement(locator) { const element = await this.findClickableElement(locator) await element.click() } async clickPoint(locator, x, y) { const element = await this.findElement(locator) await this.driver .actions() .move({ origin: element, x, y }) .click() .perform() } async scrollToElement(element) { await this.driver.executeScript( 'arguments[0].scrollIntoView(true)', element, ) } async assertElementNotPresent(locator) { let dataTab try { dataTab = await this.findElement(locator) } catch (err) { assert( err instanceof webdriverError.NoSuchElementError || err instanceof webdriverError.TimeoutError, ) } assert.ok(!dataTab, 'Found element that should not be present') } // Navigation async navigate(page = Driver.PAGES.HOME) { return await this.driver.get(`${this.extensionUrl}/${page}.html`) } // Metrics async collectMetrics() { return await this.driver.executeScript(collectMetrics) } // Window management async openNewPage(url) { const newHandle = await this.driver.switchTo().newWindow() await this.driver.get(url) return newHandle } async switchToWindow(handle) { await this.driver.switchTo().window(handle) } async getAllWindowHandles() { return await this.driver.getAllWindowHandles() } 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') } 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} exceptions - The list of window handle exceptions * @param {Array} [windowHandles] - The full list of window handles * @returns {Promise} */ 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 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() 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(), ) await fs.writeFile( `${filepathBase}-state.json`, JSON.stringify(uiState, null, 2), ) } 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') const errorEntries = browserLogs.filter( (entry) => !ignoredLogTypes.includes(entry.level.toString()), ) const errorObjects = errorEntries.map((entry) => entry.toJSON()) return errorObjects.filter( (entry) => !ignoredErrorMessages.some((message) => entry.message.includes(message), ), ) } } function collectMetrics() { const results = { paint: {}, navigation: [], } window.performance.getEntriesByType('paint').forEach((paintEntry) => { results.paint[paintEntry.name] = paintEntry.startTime }) 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