UNPKG

@applitools/eyes.selenium

Version:

Applitools Eyes SDK for Selenium WebDriver

385 lines (358 loc) 14.5 kB
'use strict'; const command = require('selenium-webdriver/lib/command'); const { TargetLocator } = require('selenium-webdriver/lib/webdriver'); const { Location, RectangleSize, ArgumentGuard, GeneralUtils } = require('@applitools/eyes.sdk.core'); const { Frame } = require('../frames/Frame'); const { FrameChain } = require('../frames/FrameChain'); const { ScrollPositionProvider } = require('../positioning/ScrollPositionProvider'); const { SeleniumJavaScriptExecutor } = require('../SeleniumJavaScriptExecutor'); const { EyesWebElement } = require('./EyesWebElement'); const { EyesWebElementPromise } = require('./EyesWebElementPromise'); /** * Wraps a target locator so we can keep track of which frames have been switched to. */ class EyesTargetLocator extends TargetLocator { /** * Initialized a new EyesTargetLocator object. * * @param {Logger} logger A Logger instance. * @param {EyesWebDriver} driver The WebDriver from which the targetLocator was received. * @param {TargetLocator} targetLocator The actual TargetLocator object. */ constructor(logger, driver, targetLocator) { ArgumentGuard.notNull(logger, 'logger'); ArgumentGuard.notNull(driver, 'driver'); ArgumentGuard.notNull(targetLocator, 'targetLocator'); super(driver.getRemoteWebDriver()); this._logger = logger; this._driver = driver; this._targetLocator = targetLocator; this._jsExecutor = new SeleniumJavaScriptExecutor(driver); this._scrollPosition = new ScrollPositionProvider(this._logger, this._jsExecutor); } // noinspection JSCheckFunctionSignatures /** * Schedules a command to switch the focus of all future commands to another frame on the page. The target frame may * be specified as one of the following: * * - A number that specifies a (zero-based) index into [window.frames]( * https://developer.mozilla.org/en-US/docs/Web/API/Window.frames). * - A string, which correspond to a `id` or `name` of element. * - A {@link WebElement} reference, which correspond to a `frame` or `iframe` DOM element. * - The `null` value, to select the topmost frame on the page. Passing `null` * is the same as calling {@link #defaultContent defaultContent()}. * * @override * @param {number|string|WebElement|null} arg1 The frame locator. * @return {Promise<EyesWebDriver>} */ frame(arg1) { const that = this; if (!arg1) { that._logger.verbose('EyesTargetLocator.frame(null)'); return that.defaultContent(); } if (Number.isInteger(arg1)) { const frameIndex = arg1; that._logger.verbose(`EyesTargetLocator.frame(${frameIndex})`); // Finding the target element so and reporting it using onWillSwitch. that._logger.verbose('Getting frames list...'); return that._driver.findElementsByCssSelector('frame, iframe') .then(frames => { if (frameIndex > frames.length) { throw new TypeError(`Frame index [${frameIndex}] is invalid!`); } that._logger.verbose('Done! getting the specific frame...'); that._logger.verbose('Done! Making preparations...'); return that.willSwitchToFrame(frames[frameIndex]); }) .then(() => { that._logger.verbose('Done! Switching to frame...'); return that._targetLocator.frame(frameIndex); }) .then(() => { that._logger.verbose('Done!'); return that._driver; }); } if (GeneralUtils.isString(arg1)) { const frameNameOrId = arg1; that._logger.verbose(`EyesTargetLocator.frame(${frameNameOrId})`); // Finding the target element so we can report it. // We use find elements(plural) to avoid exception when the element is not found. that._logger.verbose('Getting frames by name...'); let frames; return that._driver.findElementsByName(frameNameOrId) .then(framesByName => { if (framesByName.length === 0) { that._logger.verbose('No frames Found! Trying by id...'); // If there are no frames by that name, we'll try the id return that._driver.findElementsById(frameNameOrId).then(framesById => { if (framesById.length === 0) { // No such frame, bummer throw new TypeError(`No frame with name or id '${frameNameOrId}' exists!`); } return framesById; }); } return framesByName; }) .then(frames_ => { frames = frames_; that._logger.verbose('Done! Making preparations...'); return that.willSwitchToFrame(frames[0]); }) .then(() => { that._logger.verbose('Done! Switching to frame...'); let frameElement = frames[0]; if (frameElement instanceof EyesWebElement) { frameElement = frameElement.getWebElement(); } return that._targetLocator.frame(frameElement); }) .then(() => { that._logger.verbose('Done!'); return that._driver; }); } let frameElement = arg1; that._logger.verbose('EyesTargetLocator.frame(element)'); that._logger.verbose('Making preparations...'); return that.willSwitchToFrame(frameElement) .then(() => { that._logger.verbose('Done! Switching to frame...'); if (frameElement instanceof EyesWebElement) { frameElement = frameElement.getWebElement(); } return that._targetLocator.frame(frameElement); }) .then(() => { that._logger.verbose('Done!'); return that._driver; }); } /** * Change focus to the parent context. If the current context is the top level browsing context, the context remains * unchanged. * * @return {Promise<EyesWebDriver>} */ parentFrame() { const that = this; that._logger.verbose('EyesTargetLocator.parentFrame()'); if (that._driver.getFrameChain().size() !== 0) { that._logger.verbose('Making preparations...'); that._driver.getFrameChain().pop(); that._logger.verbose('Done! Switching to parent frame..'); // the command is not exists in selenium js sdk, we should define it manually that._driver.getExecutor().defineCommand('switchToParentFrame', 'POST', '/session/:sessionId/frame/parent'); // noinspection JSCheckFunctionSignatures return that._driver.schedule(new command.Command('switchToParentFrame'), 'WebDriver.switchTo().parentFrame()') .then(() => { that._logger.verbose('Done!'); return that._driver; }); } return that._driver.getPromiseFactory().resolve(that._driver); } /** * Switches into every frame in the frame chain. This is used as way to switch into nested frames (while considering * scroll) in a single call. * * @param {FrameChain} frameChain The path to the frame to switch to. * @return {Promise<EyesWebDriver>} The WebDriver with the switched context. */ framesDoScroll(frameChain) { const that = this; this._logger.verbose('EyesTargetLocator.framesDoScroll(frameChain)'); return that._driver.switchTo() .defaultContent() .then(() => frameChain.getFrames() .reduce((promise, frame) => promise.then(() => { that._logger.verbose('Scrolling by parent scroll position...'); const frameLocation = frame.getLocation(); return that._scrollPosition.setPosition(frameLocation) .then(() => { that._logger.verbose('Done! Switching to frame...'); return that._driver.switchTo().frame(frame.getReference()); }) .then(() => { that._logger.verbose('Done!'); }); }), that._driver.getPromiseFactory().resolve())) .then(() => { that._logger.verbose('Done switching into nested frames!'); return that._driver; }); } /** * Switches into every frame in the frame chain. This is used as way to switch into nested frames (while considering * scroll) in a single call. * * @param {FrameChain|string[]} obj The path to the frame to switch to. Or the path to the frame to check. This is a * list of frame names/IDs (where each frame is nested in the previous frame). * @return {Promise<EyesWebDriver>} The WebDriver with the switched context. */ frames(obj) { const that = this; if (obj instanceof FrameChain) { const frameChain = obj; that._logger.verbose('EyesTargetLocator.frames(frameChain)'); return that._driver.switchTo() .defaultContent() .then(() => frameChain.getFrames() .reduce((promise, frame) => promise .then(() => { that._logger.verbose('Switching to frame...'); return that._driver.switchTo().frame(frame.getReference()); }).then(() => { that._logger.verbose('Done!'); }), that._driver.getPromiseFactory().resolve())) .then(() => { that._logger.verbose('Done switching into nested frames!'); return that._driver; }); } if (Array.isArray(obj)) { that._logger.verbose('EyesTargetLocator.frames(framesPath)'); return obj.reduce((promise, frameNameOrId) => promise .then(() => { that._logger.verbose('Switching to frame...'); return that._driver.switchTo().frame(frameNameOrId); }) .then(() => { that._logger.verbose('Done!'); }), that._driver.getPromiseFactory().resolve()) .then(() => { that._logger.verbose('Done switching into nested frames!'); return that._driver; }); } } // noinspection JSCheckFunctionSignatures /** * Schedules a command to switch the focus of all future commands to another window. * Windows may be specified by their {@code window.name} attribute or by its handle. * * @override * @param {string} nameOrHandle The name or window handle of the window to switch focus to. * @return {Promise<EyesWebDriver>} */ window(nameOrHandle) { const that = this; that._logger.verbose('EyesTargetLocator.window()'); that._driver.getFrameChain().clear(); that._logger.verbose('Done! Switching to window...'); return that._targetLocator.window(nameOrHandle).then(() => { that._logger.verbose('Done!'); return that._driver; }); } // noinspection JSCheckFunctionSignatures /** * Schedules a command to switch focus of all future commands to the topmost frame on the page. * * @override * @return {Promise<EyesWebDriver>} */ defaultContent() { const that = this; that._logger.verbose('EyesTargetLocator.defaultContent()'); if (that._driver.getFrameChain().size() !== 0) { that._logger.verbose('Making preparations...'); that._driver.getFrameChain().clear(); that._logger.verbose('Done! Switching to default content...'); return that._targetLocator.defaultContent().then(() => { that._logger.verbose('Done!'); }); } return that._driver.getPromiseFactory().resolve(that._driver); } // noinspection JSCheckFunctionSignatures /** * Schedules a command retrieve the {@code document.activeElement} element on the current document, or * {@code document.body} if activeElement is not available. * * @override * @return {!EyesWebElementPromise} */ activeElement() { this._logger.verbose('EyesTargetLocator.activeElement()'); this._logger.verbose('Switching to element...'); // noinspection JSCheckFunctionSignatures const id = this._driver.schedule( new command.Command(command.Name.GET_ACTIVE_ELEMENT), 'WebDriver.switchTo().activeElement()' ); this._logger.verbose('Done!'); return new EyesWebElementPromise(this._logger, this._driver, id); } /** * Schedules a command to change focus to the active modal dialog, such as those opened by `window.alert()`, * `window.confirm()`, and `window.prompt()`. The returned promise will be rejected with a * {@linkplain error.NoSuchAlertError} if there are no open alerts. * * @return {!AlertPromise} The open alert. */ alert() { this._logger.verbose('EyesTargetLocator.alert()'); this._logger.verbose('Switching to alert...'); const result = this._targetLocator.alert(); this._logger.verbose('Done!'); return result; } /** * Will be called before switching into a frame. * * @param {WebElement} targetFrame The element about to be switched to. * @return {Promise<void>} */ willSwitchToFrame(targetFrame) { ArgumentGuard.notNull(targetFrame, 'targetFrame'); this._logger.verbose('willSwitchToFrame()'); this._logger.verbose('Frame'); const eyesFrame = (targetFrame instanceof EyesWebElement) ? targetFrame : new EyesWebElement(this._logger, this._driver, targetFrame); const that = this; let location, elementSize, clientSize, contentLocation, originalLocation, originalOverflow; return eyesFrame.getLocation() .then(pl => { location = new Location(pl); }) .then(() => eyesFrame.getSize() .then(ds => { elementSize = new RectangleSize(ds); })) .then(() => eyesFrame.getClientWidth() .then(clientWidth => eyesFrame.getClientHeight() .then(clientHeight => { clientSize = new RectangleSize(clientWidth, clientHeight); }))) .then(() => eyesFrame.getComputedStyleInteger('border-left-width') .then(borderLeftWidth => eyesFrame.getComputedStyleInteger('border-top-width') .then(borderTopWidth => { contentLocation = new Location(location.getX() + borderLeftWidth, location.getY() + borderTopWidth); }))) .then(() => that._scrollPosition.getCurrentPosition() .then(newLocation => { originalLocation = newLocation; })) .then(() => eyesFrame.getOverflow() .then(overflow => { originalOverflow = overflow; })) .then(() => { const frame = new Frame( that._logger, targetFrame, contentLocation, elementSize, clientSize, originalLocation, originalOverflow ); that._driver.getFrameChain().push(frame); }); } } exports.EyesTargetLocator = EyesTargetLocator;