// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Atoms for simulating user actions against the browser window.19*/2021goog.provide('bot.window');2223goog.require('bot');24goog.require('bot.Error');25goog.require('bot.ErrorCode');26goog.require('bot.events');27goog.require('bot.userAgent');28goog.require('goog.dom');29goog.require('goog.dom.DomHelper');30goog.require('goog.math.Coordinate');31goog.require('goog.math.Size');32goog.require('goog.style');33goog.require('goog.userAgent');34goog.require('goog.userAgent.product');353637/**38* Whether the value of history.length includes a newly loaded page. If not,39* after a new page load history.length is the number of pages that have loaded,40* minus 1, but becomes the total number of pages on a subsequent back() call.41* @private {boolean}42* @const43*/44bot.window.HISTORY_LENGTH_INCLUDES_NEW_PAGE_ = !goog.userAgent.IE;454647/**48* Whether value of history.length includes the pages ahead of the current one49* in the history. If not, history.length equals the number of prior pages.50* Here is the WebKit bug for this behavior that was fixed by version 533:51* https://bugs.webkit.org/show_bug.cgi?id=2447252* @private {boolean}53* @const54*/55bot.window.HISTORY_LENGTH_INCLUDES_FORWARD_PAGES_ =56!goog.userAgent.WEBKIT || bot.userAgent.isEngineVersion('533');575859/**60* Screen orientation values. From the draft W3C spec at:61* http://www.w3.org/TR/2012/WD-screen-orientation-2012052262*63* @enum {string}64*/65bot.window.Orientation = {66PORTRAIT: 'portrait-primary',67PORTRAIT_SECONDARY: 'portrait-secondary',68LANDSCAPE: 'landscape-primary',69LANDSCAPE_SECONDARY: 'landscape-secondary'70};717273/**74* Returns the degrees corresponding to the orientation input.75*76* @param {!bot.window.Orientation} orientation The orientation.77* @return {number} The orientation degrees.78* @private79*/80bot.window.getOrientationDegrees_ = (function () {81var orientationMap;82return function (orientation) {83if (!orientationMap) {84orientationMap = {};85if (goog.userAgent.MOBILE) {86// The iPhone and Android phones do not change orientation event when87// held upside down. Hence, PORTRAIT_SECONDARY is not set.88orientationMap[bot.window.Orientation.PORTRAIT] = 0;89orientationMap[bot.window.Orientation.LANDSCAPE] = 90;90orientationMap[bot.window.Orientation.LANDSCAPE_SECONDARY] = -90;91if (goog.userAgent.product.IPAD) {92orientationMap[bot.window.Orientation.PORTRAIT_SECONDARY] = 180;93}94} else if (goog.userAgent.product.ANDROID) {95// Unlike the iPad, Android tablets treat landscape orientation as the96// default, i.e., having window.orientation = 0.97orientationMap[bot.window.Orientation.PORTRAIT] = -90;98orientationMap[bot.window.Orientation.LANDSCAPE] = 0;99orientationMap[bot.window.Orientation.PORTRAIT_SECONDARY] = 90;100orientationMap[bot.window.Orientation.LANDSCAPE_SECONDARY] = 180;101}102}103return orientationMap[orientation];104};105})();106107108/**109* Go back in the browser history. The number of pages to go back can110* optionally be specified and defaults to 1.111*112* @param {number=} opt_numPages Number of pages to go back.113*/114bot.window.back = function (opt_numPages) {115// Relax the upper bound by one for browsers that do not count116// newly loaded pages towards the value of window.history.length.117var maxPages = bot.window.HISTORY_LENGTH_INCLUDES_NEW_PAGE_ ?118bot.getWindow().history.length - 1 : bot.getWindow().history.length;119var numPages = bot.window.checkNumPages_(maxPages, opt_numPages);120bot.getWindow().history.go(-numPages);121};122123124/**125* Go forward in the browser history. The number of pages to go forward can126* optionally be specified and defaults to 1.127*128* @param {number=} opt_numPages Number of pages to go forward.129*/130bot.window.forward = function (opt_numPages) {131// Do not check the upper bound (use null for infinity) for browsers that132// do not count forward pages towards the value of window.history.length.133var maxPages = bot.window.HISTORY_LENGTH_INCLUDES_FORWARD_PAGES_ ?134bot.getWindow().history.length - 1 : null;135var numPages = bot.window.checkNumPages_(maxPages, opt_numPages);136bot.getWindow().history.go(numPages);137};138139140/**141* @param {?number} maxPages Upper bound on number of pages; null for infinity.142* @param {number=} opt_numPages Number of pages to move in history.143* @return {number} Correct number of pages to move in history.144* @private145*/146bot.window.checkNumPages_ = function (maxPages, opt_numPages) {147var numPages = goog.isDef(opt_numPages) ? opt_numPages : 1;148if (numPages <= 0) {149throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,150'number of pages must be positive');151}152if (maxPages !== null && numPages > maxPages) {153throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,154'number of pages must be less than the length of the browser history');155}156return numPages;157};158159160/**161* Determine the size of the window that a user could interact with. This will162* be the greatest of document.body.(width|scrollWidth), the same for163* document.documentElement or the size of the viewport.164*165* @param {!Window=} opt_win Window to determine the size of. Defaults to166* bot.getWindow().167* @return {!goog.math.Size} The calculated size.168*/169bot.window.getInteractableSize = function (opt_win) {170var win = opt_win || bot.getWindow();171var doc = win.document;172var elem = doc.documentElement;173var body = doc.body;174if (!body) {175throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,176'No BODY element present');177}178179var widths = [180elem.clientWidth, elem.scrollWidth, elem.offsetWidth,181body.scrollWidth, body.offsetWidth182];183var heights = [184elem.clientHeight, elem.scrollHeight, elem.offsetHeight,185body.scrollHeight, body.offsetHeight186];187188var width = Math.max.apply(null, widths);189var height = Math.max.apply(null, heights);190191return new goog.math.Size(width, height);192};193194195/**196* Gets the frame element.197*198* @param {!Window} win Window of the frame. Defaults to bot.getWindow().199* @return {Element} The frame element if it exists, null otherwise.200* @private201*/202bot.window.getFrame_ = function (win) {203try {204// On IE, accessing the frameElement of a popup window results in a "No205// Such interface" exception.206return win.frameElement;207} catch (e) {208return null;209}210};211212213/**214* Determine the outer size of the window.215*216* @param {!Window=} opt_win Window to determine the size of. Defaults to217* bot.getWindow().218* @return {!goog.math.Size} The calculated size.219*/220bot.window.getSize = function (opt_win) {221var win = opt_win || bot.getWindow();222var frame = bot.window.getFrame_(win);223if (bot.userAgent.ANDROID_PRE_ICECREAMSANDWICH) {224if (frame) {225// Early Android browsers do not account for border width.226var box = goog.style.getBorderBox(frame);227return new goog.math.Size(frame.clientWidth - box.left - box.right,228frame.clientHeight);229} else {230// A fixed popup size.231return new goog.math.Size(320, 240);232}233} else if (frame) {234return new goog.math.Size(frame.clientWidth, frame.clientHeight);235} else {236var docElem = win.document.documentElement;237var body = win.document.body;238var width = win.outerWidth || (docElem && docElem.clientWidth) ||239(body && body.clientWidth) || 0;240var height = win.outerHeight || (docElem && docElem.clientHeight) ||241(body && body.clientHeight) || 0;242return new goog.math.Size(width, height);243}244};245246247/**248* Set the outer size of the window.249*250* @param {!goog.math.Size} size The new window size.251* @param {!Window=} opt_win Window to determine the size of. Defaults to252* bot.getWindow().253*/254bot.window.setSize = function (size, opt_win) {255var win = opt_win || bot.getWindow();256var frame = bot.window.getFrame_(win);257if (frame) {258// minHeight and minWidth are altered because many browsers will not change259// height or width if it is less than a specified minHeight or minWidth.260frame.style.minHeight = '0px';261frame.style.minWidth = '0px';262frame.width = size.width + 'px';263frame.style.width = size.width + 'px';264frame.height = size.height + 'px';265frame.style.height = size.height + 'px';266} else {267win.resizeTo(size.width, size.height);268}269};270271272/**273* Determine the scroll position of the window.274*275* @param {!Window=} opt_win Window to determine the scroll position of.276* Defaults to bot.getWindow().277* @return {!goog.math.Coordinate} The scroll position.278*/279bot.window.getScroll = function (opt_win) {280var win = opt_win || bot.getWindow();281return new goog.dom.DomHelper(win.document).getDocumentScroll();282};283284285/**286* Set the scroll position of the window.287*288* @param {!goog.math.Coordinate} position The new scroll position.289* @param {!Window=} opt_win Window to apply position to. Defaults to290* bot.getWindow().291*/292bot.window.setScroll = function (position, opt_win) {293var win = opt_win || bot.getWindow();294win.scrollTo(position.x, position.y);295};296297298/**299* Get the position of the window.300*301* @param {!Window=} opt_win Window to determine the position of. Defaults to302* bot.getWindow().303* @return {!goog.math.Coordinate} The position of the window.304*/305bot.window.getPosition = function (opt_win) {306var win = opt_win || bot.getWindow();307var x, y;308309if (goog.userAgent.IE) {310x = win.screenLeft;311y = win.screenTop;312} else {313x = win.screenX;314y = win.screenY;315}316317return new goog.math.Coordinate(x, y);318};319320321/**322* Set the position of the window.323*324* @param {!goog.math.Coordinate} position The target position.325* @param {!Window=} opt_win Window to set the position of. Defaults to326* bot.getWindow().327*/328bot.window.setPosition = function (position, opt_win) {329var win = opt_win || bot.getWindow();330win.moveTo(position.x, position.y);331};332333334/**335* Scrolls the given position into the viewport, using the minimal amount of336* scrolling necessary to being the coordinate into view.337*338* @param {!goog.math.Coordinate} position The position to scroll into view.339* @param {!Window=} opt_win Window to apply position to. Defaults to340* bot.getWindow().341*/342bot.window.scrollIntoView = function (position, opt_win) {343var win = opt_win || bot.getWindow();344var viewport = goog.dom.getViewportSize(win);345var scroll = bot.window.getScroll(win);346347// Scroll the minimal amount to bring the position into view.348var targetScroll = new goog.math.Coordinate(349newScrollDim(position.x, scroll.x, viewport.width),350newScrollDim(position.y, scroll.y, viewport.height));351if (!goog.math.Coordinate.equals(targetScroll, scroll)) {352bot.window.setScroll(targetScroll, win);353}354355// It is difficult to determine the size of the web page in some browsers.356// We check if the scrolling we intended to do really happened. If not we357// assume that the target location is not on the web page.358if (!goog.math.Coordinate.equals(targetScroll, bot.window.getScroll(win))) {359throw new bot.Error(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS,360'The target scroll location ' + targetScroll + ' is not on the page.');361}362363function newScrollDim(positionDim, scrollDim, viewportDim) {364if (positionDim < scrollDim) {365return positionDim;366} else if (positionDim >= scrollDim + viewportDim) {367return positionDim - viewportDim + 1;368} else {369return scrollDim;370}371}372};373374375/**376* @return {number} The current window orientation degrees.377* window.378* @private379*/380bot.window.getCurrentOrientationDegrees_ = function () {381var win = bot.getWindow();382if (!goog.isDef(win.orientation)) {383// If window.orientation is not defined, assume a default orientation of 0.384// A value of 0 indicates a portrait orientation except for android tablets385// where 0 indicates a landscape orientation.386win.orientation = 0;387}388return win.orientation;389};390391392/**393* Changes window orientation.394*395* @param {!bot.window.Orientation} orientation The new orientation of the396* window.397*/398bot.window.changeOrientation = function (orientation) {399var win = bot.getWindow();400var currentOrientationDegrees = bot.window.getCurrentOrientationDegrees_();401var newOrientationDegrees = bot.window.getOrientationDegrees_(orientation);402if (currentOrientationDegrees == newOrientationDegrees ||403!goog.isDef(newOrientationDegrees)) {404return;405}406407// If possible, try to override the window's orientation value.408// On some older version of Android, it's not possible to change409// the window's orientation value.410if (Object.getOwnPropertyDescriptor && Object.defineProperty) {411var descriptor = Object.getOwnPropertyDescriptor(win, 'orientation');412if (descriptor && descriptor.configurable) {413Object.defineProperty(win, 'orientation', {414configurable: true,415get: function () {416return newOrientationDegrees;417}418});419}420}421bot.events.fire(win, bot.events.EventType.ORIENTATIONCHANGE);422423// Change the window size to reflect the new orientation.424if (Math.abs(currentOrientationDegrees - newOrientationDegrees) % 180 != 0) {425var size = bot.window.getSize();426var shorter = size.getShortest();427var longer = size.getLongest();428if (orientation == bot.window.Orientation.PORTRAIT ||429orientation == bot.window.Orientation.PORTRAIT_SECONDARY) {430bot.window.setSize(new goog.math.Size(shorter, longer));431} else {432bot.window.setSize(new goog.math.Size(longer, shorter));433}434}435};436437438