Skip to content

Instantly share code, notes, and snippets.

@illicitonion
Created October 26, 2011 23:17
Show Gist options
  • Select an option

  • Save illicitonion/1318309 to your computer and use it in GitHub Desktop.

Select an option

Save illicitonion/1318309 to your computer and use it in GitHub Desktop.
Scrolling
Index: javascript/atoms/test/scrolling_test.html
===================================================================
--- javascript/atoms/test/scrolling_test.html (revision 0)
+++ javascript/atoms/test/scrolling_test.html (revision 0)
@@ -0,0 +1,130 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <title>scrolling_test.html</title>
+ <script src="test_bootstrap.js"></script>
+ <script type="text/javascript">
+ goog.require('bot');
+ goog.require('bot.action');
+ goog.require('bot.dom');
+ goog.require('goog.events');
+ goog.require('goog.testing.AsyncTestCase');
+ goog.require('goog.testing.jsunit');
+ goog.require('goog.userAgent');
+ </script>
+</head>
+<body>
+ <script type="text/javascript">
+ var findElement = bot.locators.findElement;
+ var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
+
+ function setUp() {
+ goog.events.removeAll();
+ window.scrollTo(0, 0);
+ assertEquals(document.body.scrollLeft, 0);
+ assertEquals(document.body.scrollTop, 0);
+ }
+
+ function tearDown() {
+ window.scrollTo(0, 0); //Convenience to view results
+ }
+
+ function getTopLevelParent() {
+ var document = bot.getDocument();
+ return goog.userAgent.WEBKIT ? document.body : document.documentElement;
+ }
+
+ function testScrollsElementInToViewIfRequired() {
+ bot.action.click(findElement({id: 'offscreen'}));
+ assertNotEquals(0, getTopLevelParent().scrollLeft);
+ }
+
+ function testDoesNotScrollElementInToViewIfAlreadyVisible() {
+ bot.action.click(findElement({id: 'onscreen'}));
+ assertEquals(0, getTopLevelParent().scrollLeft);
+ }
+
+ function testClicksElementIfHiddenByScrollableOverflow() {
+ var element = findElement({id: 'scrollable'});
+ assertClickClicks(element);
+ }
+
+ function testDoesNotScrollContainerIfNotHiddenByOverflow() {
+ bot.action.click(findElement({id: 'scrollable-in-view'}));
+ assertEquals(0, findElement({id: 'scrollable-in-view-container'}).scrollLeft);
+ }
+
+ function testScrollsContainerIfHiddenByOverflow() {
+ bot.action.click(findElement({id: 'scrollable'}));
+ assertNotEquals(0, findElement({id: 'scrollable-container'}).scrollLeft);
+ }
+
+ function testDoesNotClickElementHiddenByOverflow() {
+ var element = findElement({id: 'hidden-by-overflow'});
+ goog.events.listenOnce(element, 'click', function() {
+ fail('Expected not to click on element');
+ });
+
+ asyncTestCase.waitForAsync('Waiting for click event not to fire');
+
+ try {
+ bot.action.click(element);
+ fail('Expected exception');
+ } catch (e) {
+ assertEquals(bot.ErrorCode.ELEMENT_NOT_VISIBLE, e.code);
+ }
+
+ window.setTimeout(function() { asyncTestCase.continueTesting(); }, 100);
+ }
+
+ function testClicksElementVisibleInHiddenOverflow() {
+ var element = findElement({id: 'not-hidden-by-hidden-overflow'});
+ assertClickClicks(element);
+ }
+
+ function testScrollsNestedScrollContainers() {
+ var top = findElement({id: 'nested-top-container'});
+ var middle = findElement({id: 'nested-inner-container'});
+ var element = findElement({id: 'nested'});
+ assertClickClicks(element);
+ assertNotEquals(0, top.scrollLeft);
+ assertNotEquals(0, middle.scrollLeft);
+ }
+
+ function testScrollsNestedScrollContainersInIframes() {
+ var iframe = findElement({id: 'scrollable-iframe'});
+ var frameDocument = iframe.contentWindow.document;
+ var top = findElement({id: 'iframe-scrollable-top-container'}, frameDocument);
+ var middle = findElement({id: 'iframe-scrollable-nested-container'}, frameDocument);
+ var element = findElement({id: 'iframe-scrollable'}, frameDocument);
+ assertClickClicks(element);
+ assertNotEquals(0, frameDocument.body.scrollLeft);
+ assertNotEquals(0, top.scrollLeft);
+ assertNotEquals(0, middle.scrollLeft);
+ }
+
+ function assertClickClicks(element) {
+ var clicked = 0;
+ goog.events.listenOnce(element, 'click', function() {
+ asyncTestCase.continueTesting();
+ });
+
+ asyncTestCase.waitForAsync('Waiting for click event not to fire');
+
+ bot.action.click(element);
+ }
+ </script>
+ <div style="position: absolute; left: 150px; top: 100px; border: 2px yellow solid;"><a id="onscreen">Onscreen</a></div>
+ <div style="position: absolute; left: 3000px; border: 2px orange solid;"><a id="offscreen">Offscreen</a></div>
+ <div style="overflow: hidden; width: 50px; height: 30px; border: 2px blue solid;">
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="hidden-by-overflow">Hidden</a></div>
+ </div>
+ <div id="scrollable-container" style="overflow: scroll; width: 100px; height: 100px; border: 2px purple solid;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="scrollable">Scrollable</a></div>
+ <div id="scrollable-in-view-container" style="overflow: scroll; width: 150px; height: 50px; border: 2px grey solid;">&nbsp;&nbsp;&nbsp;<a id="scrollable-in-view">Scrollable&nbsp;in&nbsp;view</a></div>
+ <div style="overflow: hidden; width: 100px; height: 30px; border: 2px black solid;" id="not-hidden-by-hidden-overflow-container">&nbsp;&nbsp;&nbsp;<a id="not-hidden-by-hidden-overflow">Not hidden</a></div>
+ <div style="overflow: scroll; width: 100px; height: 100px;" id="nested-top-container">
+ <div style="overflow: scroll; width: 150px; left: 150px; margin-left: 200px;" id="nested-inner-container">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="nested">Nested</a></div>
+ </div>
+ <iframe src="testdata/scrolling_iframe.html" style="width: 200px; height: 100px;" id="scrollable-iframe"></iframe>
+</body>
+</html>
Index: javascript/atoms/test/testdata/scrolling_iframe.html
===================================================================
--- javascript/atoms/test/testdata/scrolling_iframe.html (revision 0)
+++ javascript/atoms/test/testdata/scrolling_iframe.html (revision 0)
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <title>Nested scrollable</title>
+</head>
+<body>
+ <div id="iframe-scrollable-top-container" style="overflow: scroll; margin-left: 200px; width: 150px; border: 2px orange solid;">
+ <div id="iframe-scrollable-nested-container" style="overflow: scroll; margin-left: 200px; width: 100px;">
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="iframe-scrollable">iFrameScrollable</a>
+ </div>
+ </div>
+</body>
+</html>
Index: javascript/atoms/action.js
===================================================================
--- javascript/atoms/action.js (revision 14329)
+++ javascript/atoms/action.js (working copy)
@@ -36,6 +36,7 @@
goog.require('goog.dom.TagName');
goog.require('goog.events.EventType');
goog.require('goog.math.Coordinate');
+goog.require('goog.math.Rect');
goog.require('goog.userAgent');
@@ -498,29 +499,31 @@
/**
- * A helper function which prepares the mouse for a click action. It checks if
- * the the element is shown, scrolls the element into few, sets the
- * {@code opt_coords} if they are undefined, and moves the mouse to the right
- * position.
+ * Ensures that the click-point of the passed element is displayed in the
+ * browser, ready to be clicked. Throws if element cannot be made to be
+ * displayed.
*
- * @param {!Element} element The element to click.
+ * @param {!Element} element The element being clicked.
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
- * target.
- * @return {!bot.Mouse} The mouse object used for the click.
- * @private
+ * target element.
+ * @return {!goog.math.Coordinate} Coordinate guaranteed to be in view,
+ * relative to the target element.
*/
-bot.action.prepareMouseForClick_ = function(element, opt_coords) {
+bot.action.scrollClickPointInView = function(element, opt_coords) {
if (!bot.dom.isShown(element, true)) {
throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_VISIBLE,
'Element is not currently visible and may not be manipulated');
}
- // Unlike element.scrollIntoView(), this scrolls the minimal amount
- // necessary, not scrolling at all if the element is already in view.
- var doc = goog.dom.getOwnerDocument(element);
- goog.style.scrollIntoContainerView(element,
- goog.userAgent.WEBKIT ? doc.body : doc.documentElement);
+ if (!opt_coords) {
+ var size = goog.style.getSize(element);
+ opt_coords = new goog.math.Coordinate(size.width / 2, size.height / 2);
+ }
+ var rectToFocus = new goog.math.Rect(opt_coords.x, opt_coords.y, 1, 1);
+
+ bot.dom.scrollElementRegionIntoClientView(element, rectToFocus);
+
// NOTE(user): Ideally, we would check that any provided coordinates fall
// within the bounds of the element, but this has proven difficult, because:
// (1) Browsers sometimes lie about the true size of elements, e.g. when text
@@ -529,11 +532,25 @@
// (2) Elements with children styled as position:absolute will often not have
// a bounding box that surrounds all of their children, but it is useful for
// the user to be able to interact with this parent element as if it does.
- if (!opt_coords) {
- var size = goog.style.getSize(element);
- opt_coords = new goog.math.Coordinate(size.width / 2, size.height / 2);
- }
+ return opt_coords;
+};
+
+
+/**
+ * A helper function which prepares the mouse for a click action. It checks if
+ * the the element is shown, scrolls the element into few, sets the
+ * {@code opt_coords} if they are undefined, and moves the mouse to the right
+ * position.
+ *
+ * @param {!Element} element The element to click.
+ * @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
+ * target.
+ * @return {!bot.Mouse} The mouse object used for the click.
+ * @private
+ */
+bot.action.prepareMouseForClick_ = function(element, opt_coords) {
+ opt_coords = bot.action.scrollClickPointInView(element, opt_coords);
var mouse = new bot.Mouse();
mouse.move(element, opt_coords);
Index: javascript/atoms/dom.js
===================================================================
--- javascript/atoms/dom.js (revision 14329)
+++ javascript/atoms/dom.js (working copy)
@@ -980,10 +980,20 @@
* @private
*/
bot.dom.scrollRegionIntoView_ = function(region, scrollable) {
- scrollable.scrollLeft += Math.min(
- region.left, Math.max(region.left - region.width, 0));
- scrollable.scrollTop += Math.min(
- region.top, Math.max(region.top - region.height, 0));
+ var leftScroll = Math.min(region.left,
+ Math.max(region.left - region.width, 0));
+ var topScroll = Math.min(region.top,
+ Math.max(region.top - region.height, 0));
+
+
+ var style = goog.style.getComputedStyle(scrollable, 'overflow');
+ if (style == 'hidden' && (leftScroll != 0 || topScroll != 0)) {
+ throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_VISIBLE,
+ 'Element cannot be scrolled in to view, and may not be manipulated');
+ }
+
+ scrollable.scrollLeft += leftScroll;
+ scrollable.scrollTop += topScroll;
};
@@ -1033,12 +1043,12 @@
* scrolled into view.
* @private
*/
-bot.dom.scrollElementRegionIntoClientView_ = function(elem, elemRegion) {
+bot.dom.scrollElementRegionIntoClientView = function(elem, elemRegion) {
var doc = goog.dom.getOwnerDocument(elem);
// Scroll the containers.
for (var container = bot.dom.getParentElement(elem);
- container && container != doc.body && container != doc.documentElement;
+ container;
container = bot.dom.getParentElement(container)) {
bot.dom.scrollElementRegionIntoContainerView_(elem, elemRegion, container);
}
@@ -1081,7 +1091,7 @@
} else {
elemRegion = new goog.math.Rect(0, 0, elem.offsetWidth, elem.offsetHeight);
}
- bot.dom.scrollElementRegionIntoClientView_(elem, elemRegion);
+ bot.dom.scrollElementRegionIntoClientView(elem, elemRegion);
// This is needed for elements that are split across multiple lines.
var rect = elem.getClientRects ? elem.getClientRects()[0] : null;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment