Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 GEN('#include "chrome/browser/ui/webui/options/options_browsertest.h"');
      6 
      7 /** @const */ var MANAGED_USERS_PREF = 'profile.managed_users';
      8 
      9 /**
     10  * Wait for the method specified by |methodName|, on the |object| object, to be
     11  * called, then execute |afterFunction|.
     12  * @param {*} object Object with callable property named |methodName|.
     13  * @param {string} methodName The name of the property on |object| to use as a
     14  *     callback.
     15  * @param {!Function} afterFunction A function to call after object.methodName()
     16  *     is called.
     17  */
     18 function waitForResponse(object, methodName, afterFunction) {
     19   var originalCallback = object[methodName];
     20 
     21   // Install a wrapper that temporarily replaces the original function.
     22   object[methodName] = function() {
     23     object[methodName] = originalCallback;
     24     originalCallback.apply(this, arguments);
     25     afterFunction();
     26   };
     27 }
     28 
     29 /**
     30   * Wait for the global window.onpopstate callback to be called (after a tab
     31   * history navigation), then execute |afterFunction|.
     32   * @param {!Function} afterFunction A function to call after pop state events.
     33   */
     34 function waitForPopstate(afterFunction) {
     35   waitForResponse(window, 'onpopstate', afterFunction);
     36 }
     37 
     38 /**
     39  * TestFixture for OptionsPage WebUI testing.
     40  * @extends {testing.Test}
     41  * @constructor
     42  */
     43 function OptionsWebUITest() {}
     44 
     45 OptionsWebUITest.prototype = {
     46   __proto__: testing.Test.prototype,
     47 
     48   /** @override */
     49   accessibilityIssuesAreErrors: true,
     50 
     51   /** @override */
     52   setUp: function() {
     53     // user-image-stream is a streaming video element used for capturing a
     54     // user image during OOBE.
     55     this.accessibilityAuditConfig.ignoreSelectors('videoWithoutCaptions',
     56                                                   '.user-image-stream');
     57   },
     58 
     59   /**
     60    * Browse to the options page & call our preLoad().
     61    */
     62   browsePreload: 'chrome://settings-frame',
     63 
     64   isAsync: true,
     65 
     66   /**
     67    * Register a mock handler to ensure expectations are met and options pages
     68    * behave correctly.
     69    */
     70   preLoad: function() {
     71     this.makeAndRegisterMockHandler(
     72         ['defaultZoomFactorAction',
     73          'fetchPrefs',
     74          'observePrefs',
     75          'setBooleanPref',
     76          'setIntegerPref',
     77          'setDoublePref',
     78          'setStringPref',
     79          'setObjectPref',
     80          'clearPref',
     81          'coreOptionsUserMetricsAction',
     82         ]);
     83 
     84     // Register stubs for methods expected to be called before/during tests.
     85     // Specific expectations can be made in the tests themselves.
     86     this.mockHandler.stubs().fetchPrefs(ANYTHING);
     87     this.mockHandler.stubs().observePrefs(ANYTHING);
     88     this.mockHandler.stubs().coreOptionsUserMetricsAction(ANYTHING);
     89   },
     90 };
     91 
     92 // Crashes on Mac only. See http://crbug.com/79181
     93 GEN('#if defined(OS_MACOSX)');
     94 GEN('#define MAYBE_testSetBooleanPrefTriggers ' +
     95     'DISABLED_testSetBooleanPrefTriggers');
     96 GEN('#else');
     97 GEN('#define MAYBE_testSetBooleanPrefTriggers testSetBooleanPrefTriggers');
     98 GEN('#endif  // defined(OS_MACOSX)');
     99 
    100 TEST_F('OptionsWebUITest', 'MAYBE_testSetBooleanPrefTriggers', function() {
    101   // TODO(dtseng): make generic to click all buttons.
    102   var showHomeButton = $('show-home-button');
    103   var trueListValue = [
    104     'browser.show_home_button',
    105     true,
    106     'Options_Homepage_HomeButton',
    107   ];
    108   // Note: this expectation is checked in testing::Test::tearDown.
    109   this.mockHandler.expects(once()).setBooleanPref(trueListValue);
    110 
    111   // Cause the handler to be called.
    112   showHomeButton.click();
    113   showHomeButton.blur();
    114   testDone();
    115 });
    116 
    117 // Not meant to run on ChromeOS at this time.
    118 // Not finishing in windows. http://crbug.com/81723
    119 TEST_F('OptionsWebUITest', 'DISABLED_testRefreshStaysOnCurrentPage',
    120     function() {
    121   assertTrue($('search-engine-manager-page').hidden);
    122   var item = $('manage-default-search-engines');
    123   item.click();
    124 
    125   assertFalse($('search-engine-manager-page').hidden);
    126 
    127   window.location.reload();
    128 
    129   assertEquals('chrome://settings-frame/searchEngines', document.location.href);
    130   assertFalse($('search-engine-manager-page').hidden);
    131   testDone();
    132 });
    133 
    134 /**
    135  * Test the default zoom factor select element.
    136  */
    137 TEST_F('OptionsWebUITest', 'testDefaultZoomFactor', function() {
    138   // The expected minimum length of the |defaultZoomFactor| element.
    139   var defaultZoomFactorMinimumLength = 10;
    140   // Verify that the zoom factor element exists.
    141   var defaultZoomFactor = $('defaultZoomFactor');
    142   assertNotEquals(defaultZoomFactor, null);
    143 
    144   // Verify that the zoom factor element has a reasonable number of choices.
    145   expectGE(defaultZoomFactor.options.length, defaultZoomFactorMinimumLength);
    146 
    147   // Simulate a change event, selecting the highest zoom value.  Verify that
    148   // the javascript handler was invoked once.
    149   this.mockHandler.expects(once()).defaultZoomFactorAction(NOT_NULL).
    150       will(callFunction(function() { }));
    151   defaultZoomFactor.selectedIndex = defaultZoomFactor.options.length - 1;
    152   var event = {target: defaultZoomFactor};
    153   if (defaultZoomFactor.onchange) defaultZoomFactor.onchange(event);
    154   testDone();
    155 });
    156 
    157 /**
    158  * If |confirmInterstitial| is true, the OK button of the Do Not Track
    159  * interstitial is pressed, otherwise the abort button is pressed.
    160  * @param {boolean} confirmInterstitial Whether to confirm the Do Not Track
    161  *     interstitial.
    162  */
    163 OptionsWebUITest.prototype.testDoNotTrackInterstitial =
    164     function(confirmInterstitial) {
    165   Preferences.prefsFetchedCallback({'enable_do_not_track': {'value': false}});
    166   var buttonToClick = confirmInterstitial ? $('do-not-track-confirm-ok') :
    167                                             $('do-not-track-confirm-cancel');
    168   var dntCheckbox = $('do-not-track-enabled');
    169   var dntOverlay = OptionsPage.registeredOverlayPages['donottrackconfirm'];
    170   assertFalse(dntCheckbox.checked);
    171 
    172   var visibleChangeCounter = 0;
    173   var visibleChangeHandler = function() {
    174     ++visibleChangeCounter;
    175     switch (visibleChangeCounter) {
    176       case 1:
    177         window.setTimeout(function() {
    178           assertTrue(dntOverlay.visible);
    179           buttonToClick.click();
    180         }, 0);
    181         break;
    182       case 2:
    183         window.setTimeout(function() {
    184           assertFalse(dntOverlay.visible);
    185           assertEquals(confirmInterstitial, dntCheckbox.checked);
    186           dntOverlay.removeEventListener(visibleChangeHandler);
    187           testDone();
    188         }, 0);
    189         break;
    190       default:
    191         assertTrue(false);
    192     }
    193   };
    194   dntOverlay.addEventListener('visibleChange', visibleChangeHandler);
    195 
    196   if (confirmInterstitial) {
    197     this.mockHandler.expects(once()).setBooleanPref(
    198         ['enable_do_not_track', true, 'Options_DoNotTrackCheckbox']);
    199   } else {
    200     // The mock handler complains if setBooleanPref is called even though
    201     // it should not be.
    202   }
    203 
    204   dntCheckbox.click();
    205 };
    206 
    207 TEST_F('OptionsWebUITest', 'EnableDoNotTrackAndConfirmInterstitial',
    208        function() {
    209   this.testDoNotTrackInterstitial(true);
    210 });
    211 
    212 TEST_F('OptionsWebUITest', 'EnableDoNotTrackAndCancelInterstitial',
    213        function() {
    214   this.testDoNotTrackInterstitial(false);
    215 });
    216 
    217 // Check that the "Do not Track" preference can be correctly disabled.
    218 // In order to do that, we need to enable it first.
    219 TEST_F('OptionsWebUITest', 'EnableAndDisableDoNotTrack', function() {
    220   Preferences.prefsFetchedCallback({'enable_do_not_track': {'value': false}});
    221   var dntCheckbox = $('do-not-track-enabled');
    222   var dntOverlay = OptionsPage.registeredOverlayPages.donottrackconfirm;
    223   assertFalse(dntCheckbox.checked);
    224 
    225   var visibleChangeCounter = 0;
    226   var visibleChangeHandler = function() {
    227     ++visibleChangeCounter;
    228     switch (visibleChangeCounter) {
    229       case 1:
    230         window.setTimeout(function() {
    231           assertTrue(dntOverlay.visible);
    232           $('do-not-track-confirm-ok').click();
    233         }, 0);
    234         break;
    235       case 2:
    236         window.setTimeout(function() {
    237           assertFalse(dntOverlay.visible);
    238           assertTrue(dntCheckbox.checked);
    239           dntOverlay.removeEventListener(visibleChangeHandler);
    240           dntCheckbox.click();
    241         }, 0);
    242         break;
    243       default:
    244         assertNotReached();
    245     }
    246   }
    247   dntOverlay.addEventListener('visibleChange', visibleChangeHandler);
    248 
    249   this.mockHandler.expects(once()).setBooleanPref(
    250       eq(['enable_do_not_track', true, 'Options_DoNotTrackCheckbox']));
    251 
    252   var verifyCorrectEndState = function() {
    253     window.setTimeout(function() {
    254       assertFalse(dntOverlay.visible);
    255       assertFalse(dntCheckbox.checked);
    256       testDone();
    257     }, 0);
    258   }
    259   this.mockHandler.expects(once()).setBooleanPref(
    260       eq(['enable_do_not_track', false, 'Options_DoNotTrackCheckbox'])).will(
    261           callFunction(verifyCorrectEndState));
    262 
    263   dntCheckbox.click();
    264 });
    265 
    266 // Verify that preventDefault() is called on 'Enter' keydown events that trigger
    267 // the default button. If this doesn't happen, other elements that may get
    268 // focus (by the overlay closing for instance), will execute in addition to the
    269 // default button. See crbug.com/268336.
    270 TEST_F('OptionsWebUITest', 'EnterPreventsDefault', function() {
    271   var page = HomePageOverlay.getInstance();
    272   OptionsPage.showPageByName(page.name);
    273   var event = new KeyboardEvent('keydown', {
    274     'bubbles': true,
    275     'cancelable': true,
    276     'keyIdentifier': 'Enter'
    277   });
    278   assertFalse(event.defaultPrevented);
    279   page.pageDiv.dispatchEvent(event);
    280   assertTrue(event.defaultPrevented);
    281   testDone();
    282 });
    283 
    284 // Verifies that sending an empty list of indexes to move doesn't crash chrome.
    285 TEST_F('OptionsWebUITest', 'emptySelectedIndexesDoesntCrash', function() {
    286   chrome.send('dragDropStartupPage', [0, []]);
    287   setTimeout(testDone);
    288 });
    289 
    290 // This test turns out to be flaky on all platforms.
    291 // See http://crbug.com/315250.
    292 
    293 // An overlay's position should remain the same as it shows.
    294 TEST_F('OptionsWebUITest', 'DISABLED_OverlayShowDoesntShift', function() {
    295   var overlayName = 'startup';
    296   var overlay = $('startup-overlay');
    297   var frozenPages = document.getElementsByClassName('frozen');  // Gets updated.
    298   expectEquals(0, frozenPages.length);
    299 
    300   document.addEventListener('webkitTransitionEnd', function(e) {
    301     if (e.target != overlay)
    302       return;
    303 
    304     assertFalse(overlay.classList.contains('transparent'));
    305     expectEquals(numFrozenPages, frozenPages.length);
    306     testDone();
    307   });
    308 
    309   OptionsPage.navigateToPage(overlayName);
    310   var numFrozenPages = frozenPages.length;
    311   expectGT(numFrozenPages, 0);
    312 });
    313 
    314 /**
    315  * TestFixture for OptionsPage WebUI testing including tab history and support
    316  * for preference manipulation. If you don't need the features in the C++
    317  * fixture, use the simpler OptionsWebUITest (above) instead.
    318  * @extends {testing.Test}
    319  * @constructor
    320  */
    321 function OptionsWebUIExtendedTest() {}
    322 
    323 OptionsWebUIExtendedTest.prototype = {
    324   __proto__: testing.Test.prototype,
    325 
    326   /** @override */
    327   browsePreload: 'chrome://settings-frame',
    328 
    329   /** @override */
    330   typedefCppFixture: 'OptionsBrowserTest',
    331 
    332   testGenPreamble: function() {
    333     // Start with no supervised users managed by this profile.
    334     GEN('  ClearPref("' + MANAGED_USERS_PREF + '");');
    335   },
    336 
    337   /** @override */
    338   isAsync: true,
    339 
    340   /** @override */
    341   setUp: function() {
    342       // user-image-stream is a streaming video element used for capturing a
    343       // user image during OOBE.
    344       this.accessibilityAuditConfig.ignoreSelectors('videoWithoutCaptions',
    345                                                     '.user-image-stream');
    346   },
    347 
    348   /**
    349    * Asserts that two non-nested arrays are equal. The arrays must contain only
    350    * plain data types, no nested arrays or other objects.
    351    * @param {Array} expected An array of expected values.
    352    * @param {Array} result An array of actual values.
    353    * @param {boolean} doSort If true, the arrays will be sorted before being
    354    *     compared.
    355    * @param {string} description A brief description for the array of actual
    356    *     values, to use in an error message if the arrays differ.
    357    * @private
    358    */
    359   compareArrays_: function(expected, result, doSort, description) {
    360     var errorMessage = '\n' + description + ': ' + result +
    361                        '\nExpected: ' + expected;
    362     assertEquals(expected.length, result.length, errorMessage);
    363 
    364     var expectedSorted = expected.slice();
    365     var resultSorted = result.slice();
    366     if (doSort) {
    367       expectedSorted.sort();
    368       resultSorted.sort();
    369     }
    370 
    371     for (var i = 0; i < expectedSorted.length; ++i) {
    372       assertEquals(expectedSorted[i], resultSorted[i], errorMessage);
    373     }
    374   },
    375 
    376   /**
    377    * Verifies that the correct pages are currently open/visible.
    378    * @param {!Array.<string>} expectedPages An array of page names expected to
    379    *     be open, with the topmost listed last.
    380    * @param {string=} opt_expectedUrl The URL path, including hash, expected to
    381    *     be open. If undefined, the topmost (last) page name in |expectedPages|
    382    *     will be used. In either case, 'chrome://settings-frame/' will be
    383    *     prepended.
    384    * @private
    385    */
    386   verifyOpenPages_: function(expectedPages, opt_expectedUrl) {
    387     // Check the topmost page.
    388     expectEquals(null, OptionsPage.getVisibleBubble());
    389     var currentPage = OptionsPage.getTopmostVisiblePage();
    390 
    391     var lastExpected = expectedPages[expectedPages.length - 1];
    392     expectEquals(lastExpected, currentPage.name);
    393     // We'd like to check the title too, but we have to load the settings-frame
    394     // instead of the outer settings page in order to have access to
    395     // OptionsPage, and setting the title from within the settings-frame fails
    396     // because of cross-origin access restrictions.
    397     // TODO(pamg): Add a test fixture that loads chrome://settings and uses
    398     // UI elements to access sub-pages, so we can test the titles and
    399     // search-page URLs.
    400     var expectedUrl = (typeof opt_expectedUrl == 'undefined') ?
    401         lastExpected : opt_expectedUrl;
    402     var fullExpectedUrl = 'chrome://settings-frame/' + expectedUrl;
    403     expectEquals(fullExpectedUrl, window.location.href);
    404 
    405     // Collect open pages.
    406     var allPageNames = Object.keys(OptionsPage.registeredPages).concat(
    407                        Object.keys(OptionsPage.registeredOverlayPages));
    408     var openPages = [];
    409     for (var i = 0; i < allPageNames.length; ++i) {
    410       var name = allPageNames[i];
    411       var page = OptionsPage.registeredPages[name] ||
    412                  OptionsPage.registeredOverlayPages[name];
    413       if (page.visible)
    414         openPages.push(page.name);
    415     }
    416 
    417     this.compareArrays_(expectedPages, openPages, true, 'Open pages');
    418   },
    419 
    420   /*
    421    * Verifies that the correct URLs are listed in the history. Asynchronous.
    422    * @param {!Array.<string>} expectedHistory An array of URL paths expected to
    423    *     be in the tab navigation history, sorted by visit time, including the
    424    *     current page as the last entry. The base URL (chrome://settings-frame/)
    425    *     will be prepended to each. An initial 'about:blank' history entry is
    426    *     assumed and should not be included in this list.
    427    * @param {Function=} callback A function to be called after the history has
    428    *     been verified successfully. May be undefined.
    429    * @private
    430    */
    431   verifyHistory_: function(expectedHistory, callback) {
    432     var self = this;
    433     OptionsWebUIExtendedTest.verifyHistoryCallback = function(results) {
    434       // The history always starts with a blank page.
    435       assertEquals('about:blank', results.shift());
    436       var fullExpectedHistory = [];
    437       for (var i = 0; i < expectedHistory.length; ++i) {
    438         fullExpectedHistory.push(
    439             'chrome://settings-frame/' + expectedHistory[i]);
    440       }
    441       self.compareArrays_(fullExpectedHistory, results, false, 'History');
    442       callback();
    443     };
    444 
    445     // The C++ fixture will call verifyHistoryCallback with the results.
    446     chrome.send('optionsTestReportHistory');
    447   },
    448 
    449   /**
    450    * Overrides the page callbacks for the given OptionsPage overlay to verify
    451    * that they are not called.
    452    * @param {Object} overlay The singleton instance of the overlay.
    453    * @private
    454    */
    455   prohibitChangesToOverlay_: function(overlay) {
    456     overlay.initializePage =
    457         overlay.didShowPage =
    458         overlay.didClosePage = function() {
    459           assertTrue(false,
    460                      'Overlay was affected when changes were prohibited.');
    461         };
    462   },
    463 };
    464 
    465 /**
    466  * Set by verifyHistory_ to incorporate a followup callback, then called by the
    467  * C++ fixture with the navigation history to be verified.
    468  * @type {Function}
    469  */
    470 OptionsWebUIExtendedTest.verifyHistoryCallback = null;
    471 
    472 // Show the search page with no query string, to fall back to the settings page.
    473 // Test disabled because it's flaky. crbug.com/303841
    474 TEST_F('OptionsWebUIExtendedTest', 'DISABLED_ShowSearchPageNoQuery',
    475        function() {
    476   OptionsPage.showPageByName('search');
    477   this.verifyOpenPages_(['settings']);
    478   this.verifyHistory_(['settings'], testDone);
    479 });
    480 
    481 // Show a page without updating history.
    482 TEST_F('OptionsWebUIExtendedTest', 'ShowPageNoHistory', function() {
    483   this.verifyOpenPages_(['settings'], '');
    484   // There are only two main pages, 'settings' and 'search'. It's not possible
    485   // to show the search page using OptionsPage.showPageByName, because it
    486   // reverts to the settings page if it has no search text set. So we show the
    487   // search page by performing a search, then test showPageByName.
    488   $('search-field').onsearch({currentTarget: {value: 'query'}});
    489 
    490   // The settings page is also still "open" (i.e., visible), in order to show
    491   // the search results. Furthermore, the URL hasn't been updated in the parent
    492   // page, because we've loaded the chrome-settings frame instead of the whole
    493   // settings page, so the cross-origin call to set the URL fails.
    494   this.verifyOpenPages_(['settings', 'search'], 'search#query');
    495   var self = this;
    496   this.verifyHistory_(['', 'search#query'], function() {
    497     OptionsPage.showPageByName('settings', false);
    498     self.verifyOpenPages_(['settings'], 'search#query');
    499     self.verifyHistory_(['', 'search#query'], testDone);
    500   });
    501 });
    502 
    503 TEST_F('OptionsWebUIExtendedTest', 'ShowPageWithHistory', function() {
    504   // See comments for ShowPageNoHistory.
    505   $('search-field').onsearch({currentTarget: {value: 'query'}});
    506   var self = this;
    507   this.verifyHistory_(['', 'search#query'], function() {
    508     OptionsPage.showPageByName('settings', true);
    509     self.verifyOpenPages_(['settings'], '#query');
    510     self.verifyHistory_(['', 'search#query', '#query'],
    511                         testDone);
    512   });
    513 });
    514 
    515 TEST_F('OptionsWebUIExtendedTest', 'ShowPageReplaceHistory', function() {
    516   // See comments for ShowPageNoHistory.
    517   $('search-field').onsearch({currentTarget: {value: 'query'}});
    518   var self = this;
    519   this.verifyHistory_(['', 'search#query'], function() {
    520     OptionsPage.showPageByName('settings', true, {'replaceState': true});
    521     self.verifyOpenPages_(['settings'], '#query');
    522     self.verifyHistory_(['', '#query'], testDone);
    523   });
    524 });
    525 
    526 // This should be identical to ShowPageWithHisory.
    527 TEST_F('OptionsWebUIExtendedTest', 'NavigateToPage', function() {
    528   // See comments for ShowPageNoHistory.
    529   $('search-field').onsearch({currentTarget: {value: 'query'}});
    530   var self = this;
    531   this.verifyHistory_(['', 'search#query'], function() {
    532     OptionsPage.navigateToPage('settings');
    533     self.verifyOpenPages_(['settings'], '#query');
    534     self.verifyHistory_(['', 'search#query', '#query'],
    535                         testDone);
    536   });
    537 });
    538 
    539 // Settings overlays are much more straightforward than settings pages, opening
    540 // normally with none of the latter's quirks in the expected history or URL.
    541 TEST_F('OptionsWebUIExtendedTest', 'ShowOverlayNoHistory', function() {
    542   // Open a layer-1 overlay, not updating history.
    543   OptionsPage.showPageByName('languages', false);
    544   this.verifyOpenPages_(['settings', 'languages'], '');
    545 
    546   var self = this;
    547   this.verifyHistory_([''], function() {
    548     // Open a layer-2 overlay for which the layer-1 is a parent, not updating
    549     // history.
    550     OptionsPage.showPageByName('addLanguage', false);
    551     self.verifyOpenPages_(['settings', 'languages', 'addLanguage'],
    552                           '');
    553     self.verifyHistory_([''], testDone);
    554   });
    555 });
    556 
    557 TEST_F('OptionsWebUIExtendedTest', 'ShowOverlayWithHistory', function() {
    558   // Open a layer-1 overlay, updating history.
    559   OptionsPage.showPageByName('languages', true);
    560   this.verifyOpenPages_(['settings', 'languages']);
    561 
    562   var self = this;
    563   this.verifyHistory_(['', 'languages'], function() {
    564     // Open a layer-2 overlay, updating history.
    565     OptionsPage.showPageByName('addLanguage', true);
    566     self.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    567     self.verifyHistory_(['', 'languages', 'addLanguage'], testDone);
    568   });
    569 });
    570 
    571 TEST_F('OptionsWebUIExtendedTest', 'ShowOverlayReplaceHistory', function() {
    572   // Open a layer-1 overlay, updating history.
    573   OptionsPage.showPageByName('languages', true);
    574   var self = this;
    575   this.verifyHistory_(['', 'languages'], function() {
    576     // Open a layer-2 overlay, replacing history.
    577     OptionsPage.showPageByName('addLanguage', true, {'replaceState': true});
    578     self.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    579     self.verifyHistory_(['', 'addLanguage'], testDone);
    580   });
    581 });
    582 
    583 // Directly show an overlay further above this page, i.e. one for which the
    584 // current page is an ancestor but not a parent.
    585 TEST_F('OptionsWebUIExtendedTest', 'ShowOverlayFurtherAbove', function() {
    586   // Open a layer-2 overlay directly.
    587   OptionsPage.showPageByName('addLanguage', true);
    588   this.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    589   var self = this;
    590   this.verifyHistory_(['', 'addLanguage'], testDone);
    591 });
    592 
    593 // Directly show a layer-2 overlay for which the layer-1 overlay is not a
    594 // parent.
    595 TEST_F('OptionsWebUIExtendedTest', 'ShowUnrelatedOverlay', function() {
    596   // Open a layer-1 overlay.
    597   OptionsPage.showPageByName('languages', true);
    598   this.verifyOpenPages_(['settings', 'languages']);
    599 
    600   var self = this;
    601   this.verifyHistory_(['', 'languages'], function() {
    602     // Open an unrelated layer-2 overlay.
    603     OptionsPage.showPageByName('cookies', true);
    604     self.verifyOpenPages_(['settings', 'content', 'cookies']);
    605     self.verifyHistory_(['', 'languages', 'cookies'], testDone);
    606   });
    607 });
    608 
    609 // Close an overlay.
    610 TEST_F('OptionsWebUIExtendedTest', 'CloseOverlay', function() {
    611   // Open a layer-1 overlay, then a layer-2 overlay on top of it.
    612   OptionsPage.showPageByName('languages', true);
    613   this.verifyOpenPages_(['settings', 'languages']);
    614   OptionsPage.showPageByName('addLanguage', true);
    615   this.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    616 
    617   var self = this;
    618   this.verifyHistory_(['', 'languages', 'addLanguage'], function() {
    619     // Close the layer-2 overlay.
    620     OptionsPage.closeOverlay();
    621     self.verifyOpenPages_(['settings', 'languages']);
    622     self.verifyHistory_(
    623         ['', 'languages', 'addLanguage', 'languages'],
    624         function() {
    625       // Close the layer-1 overlay.
    626       OptionsPage.closeOverlay();
    627       self.verifyOpenPages_(['settings'], '');
    628       self.verifyHistory_(
    629           ['', 'languages', 'addLanguage', 'languages', ''],
    630           testDone);
    631     });
    632   });
    633 });
    634 
    635 // Test that closing an overlay that did not push history when opening does not
    636 // again push history.
    637 TEST_F('OptionsWebUIExtendedTest', 'CloseOverlayNoHistory', function() {
    638   // Open the do not track confirmation prompt.
    639   OptionsPage.showPageByName('doNotTrackConfirm', false);
    640 
    641   // Opening the prompt does not add to the history.
    642   this.verifyHistory_([''], function() {
    643     // Close the overlay.
    644     OptionsPage.closeOverlay();
    645     // Still no history changes.
    646     this.verifyHistory_([''], testDone);
    647   }.bind(this));
    648 });
    649 
    650 // Make sure an overlay isn't closed (even temporarily) when another overlay is
    651 // opened on top.
    652 TEST_F('OptionsWebUIExtendedTest', 'OverlayAboveNoReset', function() {
    653   // Open a layer-1 overlay.
    654   OptionsPage.showPageByName('languages', true);
    655   this.verifyOpenPages_(['settings', 'languages']);
    656 
    657   // Open a layer-2 overlay on top. This should not close 'languages'.
    658   this.prohibitChangesToOverlay_(options.LanguageOptions.getInstance());
    659   OptionsPage.showPageByName('addLanguage', true);
    660   this.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    661   testDone();
    662 });
    663 
    664 TEST_F('OptionsWebUIExtendedTest', 'OverlayTabNavigation', function() {
    665   // Open a layer-1 overlay, then a layer-2 overlay on top of it.
    666   OptionsPage.showPageByName('languages', true);
    667   OptionsPage.showPageByName('addLanguage', true);
    668   var self = this;
    669 
    670   // Go back twice, then forward twice.
    671   self.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    672   self.verifyHistory_(['', 'languages', 'addLanguage'], function() {
    673     window.history.back();
    674     waitForPopstate(function() {
    675       self.verifyOpenPages_(['settings', 'languages']);
    676       self.verifyHistory_(['', 'languages'], function() {
    677         window.history.back();
    678         waitForPopstate(function() {
    679           self.verifyOpenPages_(['settings'], '');
    680           self.verifyHistory_([''], function() {
    681             window.history.forward();
    682             waitForPopstate(function() {
    683               self.verifyOpenPages_(['settings', 'languages']);
    684               self.verifyHistory_(['', 'languages'], function() {
    685                 window.history.forward();
    686                 waitForPopstate(function() {
    687                   self.verifyOpenPages_(
    688                       ['settings', 'languages', 'addLanguage']);
    689                   self.verifyHistory_(
    690                       ['', 'languages', 'addLanguage'], testDone);
    691                 });
    692               });
    693             });
    694           });
    695         });
    696       });
    697     });
    698   });
    699 });
    700 
    701 // Going "back" to an overlay that's a child of the current overlay shouldn't
    702 // close the current one.
    703 TEST_F('OptionsWebUIExtendedTest', 'OverlayBackToChild', function() {
    704   // Open a layer-1 overlay, then a layer-2 overlay on top of it.
    705   OptionsPage.showPageByName('languages', true);
    706   OptionsPage.showPageByName('addLanguage', true);
    707   var self = this;
    708 
    709   self.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    710   self.verifyHistory_(['', 'languages', 'addLanguage'], function() {
    711     // Close the top overlay, then go back to it.
    712     OptionsPage.closeOverlay();
    713     self.verifyOpenPages_(['settings', 'languages']);
    714     self.verifyHistory_(
    715         ['', 'languages', 'addLanguage', 'languages'],
    716         function() {
    717       // Going back to the 'addLanguage' page should not close 'languages'.
    718       self.prohibitChangesToOverlay_(options.LanguageOptions.getInstance());
    719       window.history.back();
    720       waitForPopstate(function() {
    721         self.verifyOpenPages_(['settings', 'languages', 'addLanguage']);
    722         self.verifyHistory_(['', 'languages', 'addLanguage'],
    723                             testDone);
    724       });
    725     });
    726   });
    727 });
    728 
    729 // Going back to an unrelated overlay should close the overlay and its parent.
    730 TEST_F('OptionsWebUIExtendedTest', 'OverlayBackToUnrelated', function() {
    731   // Open a layer-1 overlay, then an unrelated layer-2 overlay.
    732   OptionsPage.showPageByName('languages', true);
    733   OptionsPage.showPageByName('cookies', true);
    734   var self = this;
    735   self.verifyOpenPages_(['settings', 'content', 'cookies']);
    736   self.verifyHistory_(['', 'languages', 'cookies'], function() {
    737     window.history.back();
    738     waitForPopstate(function() {
    739       self.verifyOpenPages_(['settings', 'languages']);
    740       testDone();
    741     });
    742   });
    743 });
    744 
    745 // Verify history changes properly while the page is loading.
    746 TEST_F('OptionsWebUIExtendedTest', 'HistoryUpdatedAfterLoading', function() {
    747   var loc = location.href;
    748 
    749   document.documentElement.classList.add('loading');
    750   assertTrue(OptionsPage.isLoading());
    751   OptionsPage.navigateToPage('searchEngines');
    752   expectNotEquals(loc, location.href);
    753 
    754   document.documentElement.classList.remove('loading');
    755   assertFalse(OptionsPage.isLoading());
    756   OptionsPage.showDefaultPage();
    757   expectEquals(loc, location.href);
    758 
    759   testDone();
    760 });
    761 
    762 // A tip should be shown or hidden depending on whether this profile manages any
    763 // supervised users.
    764 TEST_F('OptionsWebUIExtendedTest', 'SupervisingUsers', function() {
    765   // We start managing no supervised users.
    766   assertTrue($('profiles-supervised-dashboard-tip').hidden);
    767 
    768   // Remove all supervised users, then add some, watching for the pref change
    769   // notifications and UI updates in each case. Any non-empty pref dictionary
    770   // is interpreted as having supervised users.
    771   chrome.send('optionsTestSetPref', [MANAGED_USERS_PREF, {key: 'value'}]);
    772   waitForResponse(BrowserOptions, 'updateManagesSupervisedUsers', function() {
    773     assertFalse($('profiles-supervised-dashboard-tip').hidden);
    774     chrome.send('optionsTestSetPref', [MANAGED_USERS_PREF, {}]);
    775     waitForResponse(BrowserOptions, 'updateManagesSupervisedUsers', function() {
    776       assertTrue($('profiles-supervised-dashboard-tip').hidden);
    777       testDone();
    778     });
    779   });
    780 });
    781 
    782 /**
    783  * TestFixture that loads the options page at a bogus URL.
    784  * @extends {OptionsWebUIExtendedTest}
    785  * @constructor
    786  */
    787 function OptionsWebUIRedirectTest() {
    788   OptionsWebUIExtendedTest.call(this);
    789 }
    790 
    791 OptionsWebUIRedirectTest.prototype = {
    792   __proto__: OptionsWebUIExtendedTest.prototype,
    793 
    794   /** @override */
    795   browsePreload: 'chrome://settings-frame/nonexistantPage',
    796 };
    797 
    798 TEST_F('OptionsWebUIRedirectTest', 'TestURL', function() {
    799   assertEquals('chrome://settings-frame/', document.location.href);
    800   this.verifyHistory_([''], testDone);
    801 });
    802