Chromium Code Reviews| Index: chrome/test/data/webui/settings/settings_transitions_browsertest.js |
| diff --git a/chrome/test/data/webui/settings/settings_transitions_browsertest.js b/chrome/test/data/webui/settings/settings_transitions_browsertest.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..ce17208a56acf8790ea58a10a004a05de9db350d |
| --- /dev/null |
| +++ b/chrome/test/data/webui/settings/settings_transitions_browsertest.js |
| @@ -0,0 +1,406 @@ |
| +// Copyright 2016 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** @fileoverview Suite of tests for Settings navigational transitions. */ |
| + |
| +GEN_INCLUDE(['settings_page_browsertest.js']); |
| + |
| +/** |
| + * @constructor |
| + * @extends {SettingsPageBrowserTest} |
| +*/ |
| +function SettingsTransitionsBrowserTest() {} |
| + |
| +/** |
| + * @typedef {{ |
| + * duration: Number, |
| + * hasStarted: function(): bool, |
| + * testIntermediateState: function(), |
| + * testEndState: function() |
|
Dan Beam
2016/08/19 02:09:16
nit: indent off
|
| + * }} |
| + */ |
| +var TransitionTest; |
| + |
| +/** @const {number} Default animation timing for neon-animated-pages. */ |
| +var kNeonAnimationsDuration = 500; |
| +/** |
| + * Max number of times to attempt waiting for a transition to end. |
| + * @const {number} |
| + */ |
| +var kMaxAttempts = 2; |
|
Dan Beam
2016/08/19 02:09:16
this is C++ style, this should probably be MAX_ATT
|
| + |
| +SettingsTransitionsBrowserTest.prototype = { |
| + __proto__: SettingsPageBrowserTest.prototype, |
| + |
| + /** @override */ |
| + browsePreload: 'chrome://md-settings/', |
| + |
| + /** |
| + * Tests a transition by waiting for it to start, running tests during its |
| + * execution, and running tests after its duration is reached, all using |
| + * callbacks -- no access to underlying animations. |
| + * @param {!TransitionTest} transitionTest |
| + * @return {!Promise} Resolved once the tests have finished. |
| + */ |
| + testTransition: function(transitionTest) { |
| + // We approximate the time when the transition started. |
| + var startedNoEarlierThan = window.performance.now(); |
| + var startedNoLaterThan; |
| + return new Promise(function(resolve, reject) { |
| + // Figure out when the transition has started with a rAF loop. Even if we |
| + // required access to underlying animation(s), there's no good way to |
| + // force a transition to start or know when it starts without spinning. |
| + (function checkIfStarted() { |
| + if (transitionTest.hasStarted()) { |
| + startedNoLaterThan = window.performance.now(); |
| + resolve(); |
| + } else { |
| + // Update the latest known time the animation wasn't running, then |
| + // check again. |
| + startedNoEarlierThan = window.performance.now(); |
| + requestAnimationFrame(checkIfStarted); |
| + } |
| + })(); |
| + }).then(function() { |
| + // Hopefully, the transition hasn't finished yet. |
| + var minDuration = window.performance.now() - startedNoLaterThan; |
| + var maxDuration = window.performance.now() - startedNoEarlierThan; |
| + assertLE(minDuration, transitionTest.duration); |
| + if (maxDuration < transitionTest.duration) { |
| + transitionTest.testIntermediateState(); |
| + } else { |
| + // We ran quite late. Don't run testIntermediateState(). |
| + console.warn('testIntermediateState() was not called because the ' + |
| + 'transition may have already ended.'); |
| + } |
| + return new Promise(function(resolve, reject) { |
| + // Wait until the transition should have completed. |
| + var numAttempts = 0; |
| + setTimeout(function tryTestEndState() { |
| + try { |
| + transitionTest.testEndState(); |
| + } catch (e) { |
| + // Try waiting a few more times to prevent flakiness. |
| + if (++numAttempts > kMaxAttempts) { |
| + console.error('Timed out waiting for testEndState to succeed'); |
| + reject(e); |
| + return; |
| + } |
| + setTimeout(tryTestEndState, transitionTest.duration); |
| + } |
| + resolve(); |
| + }, transitionTest.duration + 100); |
| + }); |
| + }); |
| + }, |
| + |
| + /** |
| + * Checks whether the subpage header (back button and title) is visible. |
| + * @param {!SettingsSubpageElement} subpage |
| + * @return {boolean} |
| + */ |
| + checkSubpageHeaderVisible: function(subpage) { |
| + var backButton = subpage.$$('[icon="settings:arrow-back"]'); |
| + assertNotEquals(null, backButton); |
| + assertGT(backButton.clientHeight, 0); |
| + var title = subpage.$$('h2'); |
| + assertNotEquals(null, title); |
| + assertGT(title.clientHeight, 0); |
| + assertGT(title.innerText.trim().length, 0); |
| + }, |
| + |
| + /** |
| + * @param {!HTMLElement} newNode The node to fade to. |
| + * @param {!HTMLElement} oldNode The node to fade from. |
| + * @return {!TransitionTest} |
| + */ |
| + getFadeTransitionTest: function(newNode, oldNode) { |
| + var newNodeStyle = getComputedStyle(newNode); |
| + var oldNodeStyle = getComputedStyle(oldNode); |
| + return { |
| + duration: kNeonAnimationsDuration, |
| + |
| + hasStarted: function() { |
| + return newNodeStyle.opacity != '0' && |
| + oldNodeStyle.opacity != '1'; |
| + }, |
| + |
| + testIntermediateState: function() { |
| + assertGT(oldNode.clientHeight, 0); |
| + assertGT(newNode.clientHeight, 0); |
| + |
| + var oldNodeOpacity = parseFloat(oldNodeStyle.opacity); |
| + assertLT(oldNodeOpacity, 1); |
| + assertGT(oldNodeOpacity, 0); |
| + var newNodeOpacity = parseFloat(newNodeStyle.opacity); |
| + assertLT(newNodeOpacity, 1); |
| + assertGT(newNodeOpacity, 0); |
| + }, |
| + |
| + testEndState: function() { |
| + assertEquals('none', oldNodeStyle.display); |
| + assertEquals('1', newNodeStyle.opacity); |
| + }, |
| + }; |
| + }, |
| + |
| + /** |
| + * Tests the transition to open a subpage from the main animatable. |
| + * @param {!SettingsSectionElement} section |
| + * @param {string} subpageId |
| + * @param {!settings.Route} subpageRoute |
| + * @return {!Promise<!SettingsSubpageElement>} Resolved with the opened |
| + * subpage once the transition completes. |
| + */ |
| + testOpenSubpage: function(section, subpageId, subpageRoute) { |
| + var page = section.domHost; |
| + |
| + var sectionStyle = getComputedStyle(section); |
| + var cardStyle = getComputedStyle(section.$.card); |
| + |
| + // Some sanity checks of starting conditions. |
| + var sectionHeightStart = section.clientHeight; |
| + var cardHeightStart = section.$.card.clientHeight; |
| + assertGT(cardHeightStart, 10); |
| + assertGT(sectionHeightStart, cardHeightStart); |
| + |
| + var mainAnimatable = this.getSectionMainAnimatable(section); |
| + assertNotEquals(null, mainAnimatable); |
| + assertGT(mainAnimatable.clientHeight, 0); |
| + var mainAnimatableStyle = getComputedStyle(mainAnimatable); |
| + |
| + this.verifySubpagesHidden(section); |
| + |
| + var otherSections = this.getSections(section.domHost); |
| + otherSections.splice(otherSections.indexOf(section), 1); |
| + assertGT(otherSections.length, 0); |
| + |
| + for (var otherSection of otherSections) |
| + assertGT(otherSection.clientHeight, 0); |
| + |
| + // Play the transition. |
| + settings.navigateTo(subpageRoute); |
| + assertEquals(subpageRoute, page.currentRoute); |
| + |
| + // The section and the card should not have changed height yet. |
| + assertEquals(section.clientHeight, sectionHeightStart); |
| + assertEquals(section.$.card.clientHeight, cardHeightStart); |
| + |
| + // The main animatable is still fully visible. |
| + assertEquals('1', mainAnimatableStyle.opacity); |
| + |
| + // The subpage should be stamped but not faded in yet. |
| + var subpages = this.getSectionSubpages(section); |
| + assertTrue(subpages.has(subpageId)); |
| + var subpage = subpages.get(subpageId); |
| + this.checkSubpageHeaderVisible(subpage); |
| + var subpageStyle = getComputedStyle(subpage); |
| + assertEquals('0', subpageStyle.opacity); |
| + |
| + var container = page.offsetParent; |
| + |
| + // Test stages of the section/card expand transition. |
| + var expandTransitionTest = { |
| + duration: EXPAND_DURATION, |
| + |
| + hasStarted: function() { |
| + // Transition starts by expanding card. |
| + return section.$.card.clientHeight > cardHeightStart; |
| + }, |
| + |
| + testIntermediateState: function() { |
| + // The card must be fixed in the viewport while expanding. |
| + assertEquals('fixed', cardStyle.position); |
| + |
| + // The card is offset to be below the toolbar. |
| + assertGT(section.$.card.offsetTop, |
| + container.getBoundingClientRect().top); |
| + |
| + // All sections retain their dimensions while the expanding section's |
| + // card expands. |
| + assertEquals(sectionHeightStart, section.clientHeight); |
| + for (var otherSection of otherSections) |
| + assertGT(otherSection.clientHeight, 0); |
| + }, |
| + |
| + testEndState: function() { |
| + // Sanity check. |
| + assertGT(section.clientHeight, sectionHeightStart); |
| + |
| + // Transition ends with the expanded section spanning the |
| + // container height at a minimum. |
| + assertEquals('100%', sectionStyle.minHeight); |
| + assertGE(section.clientHeight, container.clientHeight); |
| + |
| + // All other sections are hidden. |
| + for (var otherSection of otherSections) |
| + assertEquals(0, otherSection.clientHeight); |
| + }, |
| + }; |
| + |
| + // Test stages of the animatable/subpage fade transition. |
| + var fadeTransitionTest = |
| + this.getFadeTransitionTest(subpage, mainAnimatable); |
| + |
| + return Promise.all([ |
| + this.testTransition(expandTransitionTest), |
| + this.testTransition(fadeTransitionTest), |
| + ]).then(function() { |
| + return subpage; |
| + }); |
| + }, |
| + |
| + /** |
| + * Tests the transition to close a subpage and return to the main animatable. |
| + * @return {!Promise} Resolved once the transition |
| + * completes. |
| + */ |
| + testCloseSubpage: function(section, subpage) { |
| + var page = section.domHost; |
| + var container = page.offsetParent; |
| + |
| + var sectionStyle = getComputedStyle(section); |
| + var cardStyle = getComputedStyle(section.$.card); |
| + var subpageStyle = getComputedStyle(subpage); |
| + |
| + var sectionHeightStart = section.clientHeight; |
| + var cardHeightStart = section.$.card.clientHeight; |
| + |
| + var mainAnimatable = this.getSectionMainAnimatable(section); |
| + assertTrue(!!mainAnimatable); |
| + assertEquals(0, mainAnimatable.clientHeight); |
| + var mainAnimatableStyle = getComputedStyle(mainAnimatable); |
| + |
| + var otherSections = this.getSections(section.domHost); |
| + otherSections.splice(otherSections.indexOf(section), 1); |
| + assertGT(otherSections.length, 0); |
| + |
| + for (var otherSection of otherSections) |
| + assertEquals(0, otherSection.clientHeight); |
| + |
| + // Find the 2nd-highest route (the section itself). |
| + var route = page.currentRoute; |
| + while (route.parent.parent) |
| + route = route.parent; |
| + |
| + // Play the transition. |
| + settings.navigateTo(route); |
| + assertEquals(route, page.currentRoute); |
| + |
| + // Depending on the browser height, the card may have been scrollable. |
| + if (container.clientHeight == cardHeightStart) { |
| + // The card was not scrollable, so the height should not have changed. |
| + assertEquals(cardHeightStart, section.$.card.clientHeight); |
| + } else { |
| + // The card was scrollable (larger than the container), so it should have |
| + // been resized to the container height. |
| + assertEquals(section.$.card.clientHeight, container.clientHeight); |
| + } |
| + |
| + // The subpage is still fully visible. |
| + assertEquals('1', subpageStyle.opacity); |
| + this.checkSubpageHeaderVisible(subpage); |
| + |
| + // Test stages of the section/card collapse transition. |
| + var collapseTransitionTest = { |
| + duration: EXPAND_DURATION, |
| + |
| + hasStarted: function() { |
| + // Transition starts by collapsing card. |
| + return section.$.card.clientHeight < cardHeightStart; |
| + }, |
| + |
| + testIntermediateState: function() { |
| + // The card must be fixed in the viewport while collapsing. |
| + assertEquals('fixed', cardStyle.position); |
| + |
| + // All sections are now visible. |
| + for (var otherSection of otherSections) |
| + assertGT(otherSection.clientHeight, 0); |
| + |
| + // TODO(michaelpg): The card should be offset to be below the toolbar, |
| + // but scroll position breaks on closing the subpage: crbug.com/537359. |
| + }, |
| + |
| + testEndState: function() { |
| + // Sanity check. |
| + assertLT(section.clientHeight, sectionHeightStart); |
| + assertEquals('0px', sectionStyle.minHeight); |
| + }, |
| + }; |
| + |
| + // Test stages of the animatable/subpage fade transition. |
| + var fadeTransitionTest = |
| + this.getFadeTransitionTest(mainAnimatable, subpage); |
| + |
| + return Promise.all([ |
| + this.testTransition(collapseTransitionTest), |
| + this.testTransition(fadeTransitionTest), |
| + ]).then(function() { |
| + return subpage; |
| + }); |
| + }, |
| +}; |
| + |
| +TEST_F('SettingsTransitionsBrowserTest', 'Subpages', function() { |
| + var self = this; |
| + |
| + suite('Subpages', function() { |
| + suiteSetup(function() { |
| + var settingsUI = document.querySelector('* /deep/ settings-ui'); |
| + MockInteractions.tap(settingsUI.$.close); |
| + }); |
| + |
| + test('open and close a sub-page', function() { |
| + var page = self.getPage('basic'); |
| + assertEquals(settings.Route.BASIC, page.currentRoute); |
| + |
| + var section = self.getSection(page, 'search'); |
| + |
| + // Make the section's main page 100px shorter than the page container to |
| + // test a normal expand animation. |
| + var container = page.offsetParent; |
| + assertGT(container.clientHeight, 200, |
| + 'Browser window too short to test WebUI!'); |
| + var mainAnimatable = self.getSectionMainAnimatable(section); |
| + mainAnimatable.style.height = container.clientHeight - 100 + 'px'; |
| + section.scrollIntoView(); |
| + |
| + return self.testOpenSubpage( |
| + section, 'search-engines', settings.Route.SEARCH_ENGINES |
| + ).then(function(subpage) { |
| + // Change the subpage's height to test scrolling. |
| + subpage.style.height = '100px'; |
| + // Everything is taller than the subpage, since the section's min-height |
| + // is 100% of the container. |
| + assertGT(section.$.card.clientHeight, subpage.clientHeight); |
| + assertEquals(section.$.card.clientHeight, section.clientHeight); |
| + // The container shouldn't scroll vertically. Check the offsetHeight in |
| + // case the container has a horizontal scrollbar (small window). |
| + // TODO(michaelpg): Why is this necessary? |
| + assertEquals(container.offsetHeight, section.clientHeight); |
| + assertEquals(container.offsetHeight, container.scrollHeight); |
| + |
| + // Make sure the section and its card stretch with the content. |
| + subpage.style.height = '10000px'; |
| + assertEquals(subpage.clientHeight, section.clientHeight); |
| + assertEquals(subpage.clientHeight, section.$.card.clientHeight); |
| + |
| + // The card is now larger than the container, so the container should |
| + // scroll. |
| + assertGT(section.$.card.clientHeight, container.clientHeight); |
| + assertGT(container.scrollHeight, container.clientHeight); |
| + assertEquals(10000, container.scrollHeight); |
| + |
| + subpage.style.height = ''; |
| + return subpage; |
| + }).then(function(subpage) { |
| + // Close the subpage. |
| + return self.testCloseSubpage(section, subpage); |
| + }); |
| + }); |
| + }); |
| + |
| + mocha.run(); |
| +}); |