diff --git a/test/e2e/fixtures/anchor-scroll-y-offset/index.html b/test/e2e/fixtures/anchor-scroll-y-offset/index.html new file mode 100644 index 000000000000..e77b9b1ef3c9 --- /dev/null +++ b/test/e2e/fixtures/anchor-scroll-y-offset/index.html @@ -0,0 +1,38 @@ + + + +
+ +
+
+ Anchor {{y}} of 5 +
+ + + + + + + diff --git a/test/e2e/fixtures/anchor-scroll-y-offset/script.js b/test/e2e/fixtures/anchor-scroll-y-offset/script.js new file mode 100644 index 000000000000..260b6dc76cba --- /dev/null +++ b/test/e2e/fixtures/anchor-scroll-y-offset/script.js @@ -0,0 +1,17 @@ +angular. + module('test', []). + controller('TestController', function($anchorScroll, $location, $scope) { + $anchorScroll.yOffset = 50; + + $scope.scrollTo = function(target) { + if ($location.hash() !== target) { + // Set `$location.hash()` to `target` and + // `$anchorScroll` will detect the change and scroll + $location.hash(target); + } else { + // The hash is the same, but `target` might be out of view - + // explicitly call `$anchorScroll` + $anchorScroll(); + } + }; + }); diff --git a/test/e2e/fixtures/anchor-scroll/index.html b/test/e2e/fixtures/anchor-scroll/index.html new file mode 100644 index 000000000000..71f0d9c1624c --- /dev/null +++ b/test/e2e/fixtures/anchor-scroll/index.html @@ -0,0 +1,23 @@ + + + +
+ Go to bottom + Back to top +
+ + + + + + + diff --git a/test/e2e/fixtures/anchor-scroll/script.js b/test/e2e/fixtures/anchor-scroll/script.js new file mode 100644 index 000000000000..a83c034e9b0f --- /dev/null +++ b/test/e2e/fixtures/anchor-scroll/script.js @@ -0,0 +1,9 @@ +angular. + module('test', []). + controller('TestController', function($anchorScroll, $location, $scope) { + $scope.scrollTo = function(target) { + // Set `$location.hash()` to `target` and + // `$anchorScroll` will detect the change and scroll + $location.hash(target); + }; + }); diff --git a/test/e2e/tests/anchor-scroll.spec.js b/test/e2e/tests/anchor-scroll.spec.js new file mode 100644 index 000000000000..a5aaf44af7a3 --- /dev/null +++ b/test/e2e/tests/anchor-scroll.spec.js @@ -0,0 +1,187 @@ +describe('$anchorScroll', function() { + beforeEach(function() { + jasmine.addMatchers({ + toBeInViewport: function() { + return { + compare: function(id) { + var result = { + pass: browser.driver. + executeScript(_script_isInViewport, id). + then(function(isInViewport) { + result.message = 'Expected #' + id + (isInViewport ? ' not' : '') + + ' to be in viewport'; + return isInViewport; + }) + }; + + return result; + } + }; + }, + toHaveTop: function() { + return { + compare: function(id, expectedTop) { + var result = { + pass: browser.driver. + executeScript(_script_getTop, id). + then(function(actualTop) { + var passed = actualTop === expectedTop; + result.message = 'Expected #' + id + '\'s top' + (passed ? ' not' : '') + + ' to be ' + expectedTop + ', but it was ' + actualTop; + return passed; + }) + }; + + return result; + } + }; + } + }); + }); + + describe('basic functionality', function() { + beforeEach(function() { + loadFixture('anchor-scroll'); + }); + + it('should scroll to #bottom when clicking #top and vice versa', function() { + expect('top').toBeInViewport(); + expect('bottom').not.toBeInViewport(); + + element(by.id('top')).click(); + expect('top').not.toBeInViewport(); + expect('bottom').toBeInViewport(); + + element(by.id('bottom')).click(); + expect('top').toBeInViewport(); + expect('bottom').not.toBeInViewport(); + }); + }); + + describe('with `yOffset`', function() { + var yOffset = 50; + var buttons = element.all(by.repeater('x in [1, 2, 3, 4, 5]')); + var anchors = element.all(by.repeater('y in [1, 2, 3, 4, 5]')); + + beforeEach(function() { + loadFixture('anchor-scroll-y-offset'); + }); + + it('should scroll to the correct anchor when clicking each button', function() { + var lastAnchor = anchors.last(); + + // Make sure there is enough room to scroll the last anchor to the top + lastAnchor.getSize().then(function(size) { + var tempHeight = size.height - 10; + + execWithTempViewportHeight(tempHeight, function() { + buttons.each(function(button, idx) { + // For whatever reason, we need to run the assertions inside a callback :( + button.click().then(function() { + var anchorId = 'anchor-' + (idx + 1); + + expect(anchorId).toBeInViewport(); + expect(anchorId).toHaveTop(yOffset); + }); + }); + }); + }); + }); + + it('should automatically scroll when navigating to a URL with a hash', function() { + var lastAnchor = anchors.last(); + var lastAnchorId = 'anchor-5'; + + // Make sure there is enough room to scroll the last anchor to the top + lastAnchor.getSize().then(function(size) { + var tempHeight = size.height - 10; + + execWithTempViewportHeight(tempHeight, function() { + // Test updating `$location.url()` from within the app + expect(lastAnchorId).not.toBeInViewport(); + + browser.setLocation('#' + lastAnchorId); + expect(lastAnchorId).toBeInViewport(); + expect(lastAnchorId).toHaveTop(yOffset); + + // Test navigating to the URL directly + scrollToTop(); + expect(lastAnchorId).not.toBeInViewport(); + + browser.refresh(); + expect(lastAnchorId).toBeInViewport(); + expect(lastAnchorId).toHaveTop(yOffset); + }); + }); + }); + + it('should not scroll "overzealously"', function() { + var lastButton = buttons.last(); + var lastAnchor = anchors.last(); + var lastAnchorId = 'anchor-5'; + + // Make sure there is not enough room to scroll the last anchor to the top + lastAnchor.getSize().then(function(size) { + var tempHeight = size.height + (yOffset / 2); + + execWithTempViewportHeight(tempHeight, function() { + scrollIntoView(lastAnchorId); + expect(lastAnchorId).toHaveTop(yOffset / 2); + + lastButton.click(); + expect(lastAnchorId).toBeInViewport(); + expect(lastAnchorId).toHaveTop(yOffset); + }); + }); + }); + }); + + // Helpers + function _script_getTop(id) { + var elem = document.getElementById(id); + var rect = elem.getBoundingClientRect(); + + return rect.top; + } + + function _script_isInViewport(id) { + var elem = document.getElementById(id); + var rect = elem.getBoundingClientRect(); + var docElem = document.documentElement; + + return (rect.top < docElem.clientHeight) && + (rect.bottom > 0) && + (rect.left < docElem.clientWidth) && + (rect.right > 0); + } + + function execWithTempViewportHeight(tempHeight, fn) { + setViewportHeight(tempHeight).then(function(oldHeight) { + fn(); + setViewportHeight(oldHeight); + }); + } + + function scrollIntoView(id) { + browser.driver.executeScript('document.getElementById("' + id + '").scrollIntoView()'); + } + + function scrollToTop() { + browser.driver.executeScript('window.scrollTo(0, 0)'); + } + + function setViewportHeight(newHeight) { + return browser.driver. + executeScript('return document.documentElement.clientHeight'). + then(function(oldHeight) { + var heightDiff = newHeight - oldHeight; + var win = browser.driver.manage().window(); + + return win.getSize().then(function(size) { + return win. + setSize(size.width, size.height + heightDiff). + then(function() { return oldHeight; }); + }); + }); + } +});