/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
build-system/tasks/e2e/selenium-webdriver-controller.js
704 строки
20 KB
renovate[bot]
📦 Update build-system devDependencies (#40066)
25 июн 2024, 23:34
Не верифицирован
25 июн 2024, 23:34
45ee58a
Код
Авторство
О чём код?
const fs = require('fs'); const {By, Condition, Key: SeleniumKey, error} = require('selenium-webdriver'); const { DOMRectDef, ElementHandle, Key, ScrollToOptionsDef, } = require('./e2e-types'); const {ControllerPromise} = require('./controller-promise'); const {expect} = require('chai'); const {NetworkLogger} = require('./network-logger'); const {NoSuchElementError} = error; const ELEMENT_WAIT_TIMEOUT = 5000; /** @typedef {import('selenium-webdriver').ISize} ISize */ /** @typedef {import('selenium-webdriver').WebDriver} WebDriver */ /** @typedef {import('selenium-webdriver').WebElement} WebElement */ /** @enum {string} */ const KeyToSeleniumMap = { [Key.ArrowDown]: SeleniumKey.ARROW_DOWN, [Key.ArrowLeft]: SeleniumKey.ARROW_LEFT, [Key.ArrowRight]: SeleniumKey.ARROW_RIGHT, [Key.ArrowUp]: SeleniumKey.ARROW_UP, [Key.Backspace]: SeleniumKey.BACK_SPACE, [Key.Enter]: SeleniumKey.ENTER, [Key.Escape]: SeleniumKey.ESCAPE, [Key.Tab]: SeleniumKey.TAB, }; /** * @param {function(): !Promise<T1>} valueFn * @param {function(T2): boolean} condition * @param {function(T1): T2} opt_mutate * @return {!Condition} * @template T1 * @template T2 */ function expectCondition(valueFn, condition, opt_mutate) { opt_mutate = opt_mutate || ((x) => x); return new Condition('value matches condition', async () => { const value = await valueFn(); const mutatedValue = await opt_mutate(value); return condition(mutatedValue); }); } /** * Make the test runner wait until the value returned by the valueFn matches * the given condition. * @param {!WebDriver} driver * @param {function(): !Promise<T1>} valueFn * @param {function(T2): ?T1} condition * @param {function(T1): T2} opt_mutate * @return {!Promise<?T2>} * @template T1 * @template T2 */ async function waitFor(driver, valueFn, condition, opt_mutate) { const conditionValue = (value) => { // Box the value in an object, so values that are present but falsy // (like "") do not cause driver.wait to continue waiting. return Boolean(condition(value)); }; /* TODO: fix incorrect typing. */ /** @type {any} */ const result = await driver.wait( expectCondition(valueFn, conditionValue, opt_mutate) ); return result; } class SeleniumWebDriverController { /** * @param {!WebDriver} driver */ constructor(driver) { this.driver = driver; this.networkLogger = new NetworkLogger(driver); /** @private {?WebElement} */ this.shadowRoot_ = null; /** @private {boolean} */ this.isXpathInstalled_ = false; } /** * Return a wait function. When called, the function will cause the test * runner to wait until the given value matches the expected value. * @param {function(): !Promise<T1>} valueFn * @return {function(function(T2): ?T1, function(T1): T2): !Promise<?T2>} * @template T1 * @template T2 */ getWaitFn_(valueFn) { return (condition, opt_mutate) => { return waitFor(this.driver, valueFn, condition, opt_mutate); }; } /** * This waits for an element to become available similar to Selenium's * until.js#elementLocated * {@link https://github.com/SeleniumHQ/selenium/blob/6a717f20/javascript/node/selenium-webdriver/lib/until.js#L237} * @param {string} selector * @param {number=} timeout * @return {!Promise<!ElementHandle<!WebElement>>} */ async findElement(selector, timeout = ELEMENT_WAIT_TIMEOUT) { const bySelector = By.css(selector); const label = 'for element to be located ' + selector; const condition = new Condition(label, async () => { try { const root = await this.getRoot_(); return await root.findElement(bySelector); } catch (e) { // WebElement.prototype.findElement differs from // WebDriver.prototype.findElement in that the WebElement method will // throw a NoSuchElementError if the element is not found, and does // not wait for the element to appear in the document. if (e instanceof NoSuchElementError) { return null; } throw e; } }); const webElement = await this.driver.wait(condition, timeout); return new ElementHandle(webElement); } /** * This waits for elements to become available similar to Selenium's * until.js#elementsLocated * {@link https://github.com/SeleniumHQ/selenium/blob/6a717f20/javascript/node/selenium-webdriver/lib/until.js#L258} * * @param {string} selector * @return {!Promise<!Array<!ElementHandle<!WebElement>>>} */ async findElements(selector) { const bySelector = By.css(selector); const label = 'for at least one element to be located ' + selector; const condition = new Condition(label, async () => { try { const root = await this.getRoot_(); const elements = await root.findElements(bySelector); return elements.length ? elements : null; } catch (e) { if (e instanceof NoSuchElementError) { return null; } throw e; } }); /* TODO: fix incorrect typing. */ /** @type {WebElement[]} */ const webElements = /** @type {?} */ ( await this.driver.wait(condition, ELEMENT_WAIT_TIMEOUT) ); return webElements.map((webElement) => new ElementHandle(webElement)); } /** * @param {string} xpath * @return {!Promise<!ElementHandle<!WebElement>>} */ async findElementXPath(xpath) { await this.maybeInstallXpath_(); const label = 'for element to be located ' + xpath; const webElement = await this.driver.wait( new Condition(label, async () => { const root = await this.getRoot_(); const results = await this.evaluate( (xpath, root) => { return window.queryXpath(xpath, root); }, xpath, root ); return results && results[0]; }), ELEMENT_WAIT_TIMEOUT ); return new ElementHandle(webElement); } /** * @param {string} xpath * @return {!Promise<!Array<!ElementHandle<!WebElement>>>} */ async findElementsXPath(xpath) { await this.maybeInstallXpath_(); const label = 'for at least one element to be located ' + xpath; /* TODO: fix incorrect typing. */ /** @type {WebElement[]} */ const webElements = /** @type {?} */ ( await this.driver.wait( new Condition(label, async () => { const root = await this.getRoot_(); const results = await this.evaluate( (xpath, root) => { return window.queryXpath(xpath, root); }, xpath, root ); return results; }), ELEMENT_WAIT_TIMEOUT ) ); return webElements.map((webElement) => new ElementHandle(webElement)); } /** * Install a third-party XPath library if it is not already installed. * @return {!Promise} */ async maybeInstallXpath_() { if (this.isXpathInstalled_) { return; } this.isXpathInstalled_ = true; const scripts = await Promise.all([ fs.promises.readFile('third_party/wgxpath/wgxpath.js', 'utf8'), fs.promises.readFile( 'build-system/tasks/e2e/driver/query-xpath.js', 'utf8' ), ]); await this.driver.executeScript(scripts.join('\n\n')); } /** * @return {!Promise<!ElementHandle<!WebElement>>} */ async getActiveElement() { const root = await this.getRoot_(); const getter = (root) => root.activeElement || root.ownerDocument.activeElement; const activeElement = await this.driver.executeScript(getter, root); return new ElementHandle(activeElement); } /** * @return {!Promise<!ElementHandle<!WebElement>>} */ async getDocumentElement() { const root = await this.getRoot_(); const getter = (root) => root.ownerDocument.documentElement; const documentElement = await this.driver.executeScript(getter, root); return new ElementHandle(documentElement); } /** * @return {!ControllerPromise<string|null|function>} */ getCurrentUrl() { const waitForFn = this.getWaitFn_(() => this.driver.getCurrentUrl()); return new ControllerPromise(this.driver.getCurrentUrl(), waitForFn); } /** * @param {string} location * @return {!Promise} */ async navigateTo(location) { return this.driver.get(location); } /** * @param {!ElementHandle<!WebElement>} handle * @param {string|Key} keys * @return {!Promise} */ async type(handle, keys) { const targetElement = handle ? handle.getElement() : await this.driver.switchTo().activeElement(); if (keys === Key.CtrlV) { return this.pasteFromClipboard(); } const key = KeyToSeleniumMap[keys]; if (key) { return targetElement.sendKeys(key); } return targetElement.sendKeys(keys); } /** * Pastes from the clipboard by perfoming the keyboard shortcut. * https://stackoverflow.com/a/41046276 * @return {!Promise} */ pasteFromClipboard() { return this.driver .actions({async: false, bridge: undefined}) .keyDown(SeleniumKey.SHIFT) .keyDown(SeleniumKey.INSERT) .keyUp(SeleniumKey.SHIFT) .keyUp(SeleniumKey.INSERT) .perform(); } /** * @param {!ElementHandle<!WebElement>} handle * @return {!Promise<string>} */ getElementText(handle) { const webElement = handle.getElement(); const waitForFn = this.getWaitFn_(() => webElement.getText()); return new ControllerPromise(webElement.getText(), waitForFn); } /** * @param {!ElementHandle<!WebElement>} handle * @return {!Promise<string>} */ getElementTagName(handle) { const webElement = handle.getElement(); return webElement.getTagName(); } /** * @param {!ElementHandle<!WebElement>} handle * @param {string} attribute * @return {!Promise<string>} */ getElementAttribute(handle, attribute) { const webElement = handle.getElement(); const getter = (element, attribute) => element.getAttribute(attribute); return new ControllerPromise( this.evaluate(getter, webElement, attribute), this.getWaitFn_(() => this.evaluate(getter, webElement, attribute)) ); } /** * Gets the element property. Note that this is different * than getElementAttribute() * @param {!ElementHandle<!WebElement>} handle * @param {string} property * @return {!Promise<string>} */ getElementProperty(handle, property) { const webElement = handle.getElement(); const getProperty = (element, property) => element[property]; return new ControllerPromise( this.driver.executeScript(getProperty, webElement, property), this.getWaitFn_(() => this.driver.executeScript(getProperty, webElement, property) ) ); } /** * @param {!ElementHandle<!WebElement>} handle * @return {!Promise<!DOMRectDef>} */ getElementRect(handle) { const webElement = handle.getElement(); const getter = (element) => { // Extracting the values seems to perform better than returning // the raw ClientRect from the element, in terms of flakiness. // The raw ClientRect also has hundredths of a pixel. We round to int. const {bottom, height, left, right, top, width} = element./*OK*/ getBoundingClientRect(); return { x: Math.round(left), y: Math.round(top), width: Math.round(width), height: Math.round(height), top: Math.round(top), bottom: Math.round(bottom), left: Math.round(left), right: Math.round(right), }; }; return new ControllerPromise( this.driver.executeScript(getter, webElement), this.getWaitFn_(() => this.driver.executeScript(getter, webElement)) ); } /** * @param {!ElementHandle} handle * @param {string} styleProperty * @return {!Promise<string>} styleProperty */ getElementCssValue(handle, styleProperty) { const webElement = handle.getElement(); return new ControllerPromise( webElement.getCssValue(styleProperty), this.getWaitFn_(() => webElement.getCssValue(styleProperty)) ); } /** * @param {!ElementHandle} handle * @return {!Promise<boolean>} */ isElementEnabled(handle) { const webElement = handle.getElement(); return new ControllerPromise( webElement.isEnabled(), this.getWaitFn_(() => webElement.isEnabled()) ); } /** * @param {!ElementHandle} handle * @return {!Promise<boolean>} */ isElementDisplayed(handle) { const webElement = handle.getElement(); return new ControllerPromise( webElement.isDisplayed(), this.getWaitFn_(() => webElement.isDisplayed()) ); } /** * @return {!Promise<Array<string>>} */ async getAllWindows() { return this.driver.getAllWindowHandles(); } /** * @param {!ElementHandle} handle * @return {!Promise<boolean>} */ isElementSelected(handle) { const webElement = handle.getElement(); return new ControllerPromise( webElement.isSelected(), this.getWaitFn_(() => webElement.isSelected()) ); } /** * Get width/height of the browser area. * @return {!Promise<[number, number]>} */ async getWindowRect() { const htmlElement = this.driver.findElement(By.css('html')); return await Promise.all([ htmlElement.getAttribute('clientWidth').then(Number), htmlElement.getAttribute('clientHeight').then(Number), ]); } /** * Sets width/height of the browser area. * @param {!ISize} rect * @return {!Promise} */ async setWindowRect(rect) { const {height, width} = rect; await this.driver.manage().window().setRect({ x: 0, y: 0, width, height, }); // Check to make sure we resized the content to the correct size. const htmlElement = this.driver.findElement(By.tagName('html')); const htmlElementSizes = await Promise.all([ htmlElement.getAttribute('clientWidth'), htmlElement.getAttribute('clientHeight'), ]); // No Array destructuring allowed? const clientWidth = Number(htmlElementSizes[0]); const clientHeight = Number(htmlElementSizes[1]); // If we resized correctly, can just stop here. If the test is not run in // headless mode, we need to do more work to size correctly. if (clientWidth == width && clientHeight == height) { return; } // Calculate the window borders, so we can set the correct size to get the // desired content size. const horizBorder = width - clientWidth; const vertBorder = height - clientHeight; await this.driver .manage() .window() .setRect({ width: width + horizBorder, height: height + vertBorder, }); // Verify the size. The browser may refuse to resize smaller than some // size when not running headless. It is better to fail here rather than // have the developer wonder why their test is failing on an unrelated // expect. const updatedHtmlElementSizes = await Promise.all([ htmlElement.getAttribute('clientWidth'), htmlElement.getAttribute('clientHeight'), ]); const resultWidth = Number(updatedHtmlElementSizes[0]); const resultHeight = Number(updatedHtmlElementSizes[1]); // TODO(sparhami) These are throwing errors, but are not causing the test // to fail immediately,.Figure out why, we want the test to fail here // instead of continuing. expect(resultWidth).to.equal( width, 'Failed to resize the window to the requested width.' ); expect(resultHeight).to.equal( height, 'Failed to resize the window to the requested height.' ); } /** * Get the title of the current document. * @return {!Promise<string>} */ getTitle() { const getTitle = () => document.title; return new ControllerPromise( this.driver.executeScript(getTitle), this.getWaitFn_(() => this.driver.executeScript(getTitle)) ); } /** * * @param {!ElementHandle<!WebElement>} handle * @return {!Promise} */ async click(handle) { return handle.getElement().click(); } /** * @param {!ElementHandle<!WebElement>} handle * @param {!ScrollToOptionsDef=} opt_scrollToOptions * @return {!Promise} */ async scrollBy(handle, opt_scrollToOptions) { const webElement = handle.getElement(); const scrollBy = (element, opt_scrollToOptions) => { element./*OK*/ scrollBy(opt_scrollToOptions); }; return this.driver.executeScript(scrollBy, webElement, opt_scrollToOptions); } /** * @param {!ElementHandle<!WebElement>} handle * @param {!ScrollToOptionsDef=} opt_scrollToOptions * @return {!Promise} */ async scrollTo(handle, opt_scrollToOptions) { const webElement = handle.getElement(); const scrollTo = (element, opt_scrollToOptions) => { element./*OK*/ scrollTo(opt_scrollToOptions); }; return this.driver.executeScript(scrollTo, webElement, opt_scrollToOptions); } /** * @param {string} path * @return {!Promise<void>} An encoded string representing the image data */ async takeScreenshot(path) { const imageString = await this.driver.takeScreenshot(); fs.writeFile(path, imageString, 'base64', function () {}); } /** * Evaluate the given function * @param {function(): any} fn * @param {...*} args * @return {!Promise<*>} */ evaluate(fn, ...args) { return this.driver.executeScript(fn, ...args); } /** * @param {!ElementHandle} handle * @param {string} path * @return {!Promise<void>} An encoded string representing the image data */ async takeElementScreenshot(handle, path) { // TODO(cvializ): Errors? Or maybe ChromeDriver hasn't yet implemented // Could be https://crbug.com/chromedriver/2667 const webElement = handle.getElement(); const imageString = await webElement.takeScreenshot(); fs.writeFile(path, imageString, 'base64', function () {}); } /** * @param {string} handle * @return {!Promise} */ async switchToWindow(handle) { await this.driver.switchTo().window(handle); } /** * @param {!ElementHandle<!WebElement>} handle * @return {!Promise} */ async switchToFrame(handle) { // TODO(estherkim): add 'id' parameter, to select element inside 'handle' // use case: testing x-origin iframes like amp-mathml, amp-ima-video const element = handle.getElement(); // TODO(cvializ): This trashes cached element handles why? // await this.driver.wait(until.ableToSwitchToFrame(element)); await this.driver.switchTo().frame(element); } /** * @return {!Promise} */ async switchToParent() { // await this.driver.switchTo().parentFrame(); await this.driver.switchTo().defaultContent(); } /** * Switch controller to shadowRoot body hosted by given element. * @param {!ElementHandle<!WebElement>} handle * @return {!Promise<void>} */ async switchToShadow(handle) { const getter = (shadowHost) => shadowHost.shadowRoot.body; return this.switchToShadowInternal_(handle, getter); } /** * Switch controller to shadowRoot hosted by given element. * @param {!ElementHandle<!WebElement>} handle * @return {!Promise<void>} */ async switchToShadowRoot(handle) { const getter = (shadowHost) => shadowHost.shadowRoot; return this.switchToShadowInternal_(handle, getter); } /**. * @param {!ElementHandle<!WebElement>} handle * @param {function(): any} getter * @return {!Promise<void>} */ async switchToShadowInternal_(handle, getter) { const shadowHost = handle.getElement(); const shadowRootBody = await this.evaluate(getter, shadowHost); this.shadowRoot_ = shadowRootBody; } /** * @return {Promise<void>} */ async switchToLight() { this.shadowRoot_ = null; } /** * Get the current root * @return {!Promise<!WebElement>} */ getRoot_() { if (this.shadowRoot_) { return this.shadowRoot_; } return this.evaluate(() => document.documentElement); } /** * Shutdown the driver. * @return {Promise<void>} */ async dispose() { return this.driver.quit(); } } module.exports = { SeleniumWebDriverController, };