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