UNPKG

@applitools/dom-utils

Version:

Applitools DOM Utils is a shared utility package

273 lines (231 loc) 7.16 kB
'use strict' const axios = require('axios') const {URL} = require('url') const { ArgumentGuard, Location, GeneralUtils, PerformanceUtils, EyesError, } = require('@applitools/eyes-common') const {getCaptureDomAndPollScript} = require('@applitools/dom-capture') const DomCaptureReturnType = { OBJECT: 'OBJECT', STRING: 'STRING', } const SCRIPT_RESPONSE_STATUS = { WIP: 'WIP', ERROR: 'ERROR', SUCCESS: 'SUCCESS', } const DOM_EXTRACTION_TIMEOUT = 5 * 60 * 1000 const DOM_CAPTURE_PULL_TIMEOUT = 200 // ms const DOCUMENT_LOCATION_HREF_SCRIPT = 'return document.location.href' /** * @ignore */ class DomCapture { /** * @param {Logger} logger * @param {EyesWebDriver|WebDriver} driver */ constructor(logger, driver, script) { this._logger = logger this._driver = driver this._customScript = script } /** * @param {Logger} logger - A Logger instance. * @param {EyesWebDriver|WebDriver} driver * @param {PositionProvider} [positionProvider] * @param {DomCaptureReturnType} [returnType] * @return {Promise<string|object>} */ static async getFullWindowDom( logger, driver, positionProvider, returnType = DomCaptureReturnType.STRING, script, ) { ArgumentGuard.notNull(logger, 'logger') ArgumentGuard.notNull(driver, 'driver') let originalPosition if (positionProvider) { originalPosition = await positionProvider.getState() await positionProvider.setPosition(Location.ZERO) } const domCapture = new DomCapture(logger, driver, script) const dom = await domCapture.getWindowDom() if (positionProvider) { await positionProvider.restoreState(originalPosition) } return returnType === DomCaptureReturnType.OBJECT ? JSON.parse(dom) : dom } /** * @return {Promise<{string}>} */ async getWindowDom() { let script if (!this._customScript) { const captureDomScript = await getCaptureDomAndPollScript() script = `${captureDomScript} return __captureDomAndPoll();` } else { script = this._customScript } const url = await this._driver.getCurrentUrl() return this.getFrameDom(script, url) } /** * @param {string} script * @param {string} url * @return {Promise<{string}>} */ async getFrameDom(script, url) { let timeout, result let isCheckTimerTimedOut = false try { timeout = setTimeout(() => { isCheckTimerTimedOut = true }, DOM_EXTRACTION_TIMEOUT) do { this._logger.verbose('executing dom capture') const resultAsString = await this._driver.executeScript(script) result = JSON.parse(resultAsString) await GeneralUtils.sleep(DOM_CAPTURE_PULL_TIMEOUT) } while (result.status === SCRIPT_RESPONSE_STATUS.WIP && !isCheckTimerTimedOut) } finally { clearTimeout(timeout) } if (result.status === SCRIPT_RESPONSE_STATUS.ERROR) { throw new EyesError( `Error during capture dom and pull script: '${result.error}'`, result.error, ) } if (isCheckTimerTimedOut) { throw new EyesError('DomCapture Timed out') } const domSnapshotRawArr = result && result.value ? result.value.split('\n') : [] if (domSnapshotRawArr.length === 0) { return {} } const separatorJson = JSON.parse(domSnapshotRawArr[0]) const cssEndIndex = domSnapshotRawArr.indexOf(separatorJson.separator) const iframeEndIndex = domSnapshotRawArr.indexOf(separatorJson.separator, cssEndIndex + 1) let domSnapshot = domSnapshotRawArr[iframeEndIndex + 1] const cssArr = [] for (let i = 1; i < cssEndIndex; i += 1) { cssArr.push(domSnapshotRawArr[i]) } const cssPromises = [] for (const cssHref of cssArr) { if (cssHref) { cssPromises.push(this._downloadCss(url, cssHref)) } } const cssResArr = await Promise.all(cssPromises) for (const cssRes of cssResArr) { domSnapshot = domSnapshot.replace( `"${separatorJson.cssStartToken}${cssRes.href}${separatorJson.cssEndToken}"`, cssRes.css, ) } const iframeArr = [] for (let i = cssEndIndex + 1; i < iframeEndIndex; i += 1) { iframeArr.push(domSnapshotRawArr[i]) } for (const iframeXpath of iframeArr) { if (iframeXpath) { let domIFrame try { const originLocation = await this.getLocation() const framesCount = await this._switchToFrame(iframeXpath) const locationAfterSwitch = await this.getLocation() if (locationAfterSwitch === originLocation) { this._logger.log('Switching to frame failed') domIFrame = {} } else { domIFrame = await this.getFrameDom(script, url) await this._switchToParentFrame(framesCount) } } catch (ignored) { domIFrame = {} } domSnapshot = domSnapshot.replace( `"${separatorJson.iframeStartToken}${iframeXpath}${separatorJson.iframeEndToken}"`, domIFrame, ) } } return domSnapshot } async getLocation() { return this._driver.executeScript(DOCUMENT_LOCATION_HREF_SCRIPT) } /** * @param {string|string[]} xpaths * @return {Promise<number>} * @private */ async _switchToFrame(xpaths) { if (!Array.isArray(xpaths)) { xpaths = xpaths.split(',') } let framesCount = 0 for (const xpath of xpaths) { const iframeEl = await this._driver.findElementByXPath(`/${xpath}`) await this._driver.switchTo().frame(iframeEl) framesCount += 1 } return framesCount } /** * @private * @return {Promise<number>} */ async _switchToParentFrame(switchedToFrameCount) { if (switchedToFrameCount > 0) { await this._driver.switchTo().parentFrame() return this._switchToParentFrame(switchedToFrameCount - 1) } return switchedToFrameCount } /** * @param {string} baseUri * @param {string} href * @param {number} [retriesCount=1] * @return {Promise<{href: string, css: string}>} * @private */ async _downloadCss(baseUri, href, retriesCount = 1) { try { this._logger.verbose(`Given URL to download: ${href}`) let absHref = href if (!GeneralUtils.isAbsoluteUrl(href)) { absHref = new URL(href.toString(), baseUri).href } const timeStart = PerformanceUtils.start() const response = await axios(absHref) const css = response.data this._logger.verbose( `downloading CSS in length of ${css.length} chars took ${timeStart.end().summary}`, ) const escapedCss = GeneralUtils.cleanStringForJSON(css) return {href: absHref, css: escapedCss} } catch (ex) { this._logger.verbose(ex.toString()) retriesCount -= 1 if (retriesCount > 0) { return this._downloadCss(baseUri, href, retriesCount) } return {href, css: ''} } } getDriver() { return this._driver } } Object.freeze(DomCaptureReturnType) exports.DomCaptureReturnType = DomCaptureReturnType exports.DomCapture = DomCapture