• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5GEN('#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 */
18function 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  */
34function waitForPopstate(afterFunction) {
35  waitForResponse(window, 'onpopstate', afterFunction);
36}
37
38/**
39 * TestFixture for OptionsPage WebUI testing.
40 * @extends {testing.Test}
41 * @constructor
42 */
43function OptionsWebUITest() {}
44
45OptionsWebUITest.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
93GEN('#if defined(OS_MACOSX)');
94GEN('#define MAYBE_testSetBooleanPrefTriggers ' +
95    'DISABLED_testSetBooleanPrefTriggers');
96GEN('#else');
97GEN('#define MAYBE_testSetBooleanPrefTriggers testSetBooleanPrefTriggers');
98GEN('#endif  // defined(OS_MACOSX)');
99
100TEST_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
119TEST_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 */
137TEST_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 */
163OptionsWebUITest.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
207TEST_F('OptionsWebUITest', 'EnableDoNotTrackAndConfirmInterstitial',
208       function() {
209  this.testDoNotTrackInterstitial(true);
210});
211
212TEST_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.
219TEST_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.
270TEST_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.
285TEST_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.
294TEST_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 */
321function OptionsWebUIExtendedTest() {}
322
323OptionsWebUIExtendedTest.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 */
470OptionsWebUIExtendedTest.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
474TEST_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.
482TEST_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
503TEST_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
515TEST_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.
527TEST_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.
541TEST_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
557TEST_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
571TEST_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.
585TEST_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.
595TEST_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.
610TEST_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.
637TEST_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.
652TEST_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
664TEST_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.
703TEST_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.
730TEST_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.
746TEST_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.
764TEST_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 */
787function OptionsWebUIRedirectTest() {
788  OptionsWebUIExtendedTest.call(this);
789}
790
791OptionsWebUIRedirectTest.prototype = {
792  __proto__: OptionsWebUIExtendedTest.prototype,
793
794  /** @override */
795  browsePreload: 'chrome://settings-frame/nonexistantPage',
796};
797
798TEST_F('OptionsWebUIRedirectTest', 'TestURL', function() {
799  assertEquals('chrome://settings-frame/', document.location.href);
800  this.verifyHistory_([''], testDone);
801});
802