Added options browser_tests using the generator and js handler framework.

This patch turned out to be fairly large.  Let me describe the ultimate goal:
- To write WebUI tests in javascript, with as little C++ as possible for the simple case, yet powerful enough to support more complicated cases.  options.js illustrates the simple case, and print_preview.js illustrates the complicated case.

Original changes:
- Refactored test_tab_strip_observer into test_navigation_observer so that it could be used by itself without needing the TabInsertedAt logic, which PrintPreview needs when there's no TabContentsWrapper available.
- Added assertEqualsArray for comparing two arrays shallowly (javascript == fails).
- Provided logic in WebUIBrowserTest and in the javascript2webui.js generation script to allow browsing to a url with preload of injected javascript.
- Corrected test_navigation_observer to wait for the right notification before calling callback (which runs after the page's javascript is loaded but before its onload).
- Added guts to define OS_* ifdefs for javascript to test for disabling tests.
- Moved the handler from settings_browsertest.cc to settings.js
- use __proto__ when overriding chrome to allow other members to be seen (commandLineString, e.g.)

Additions made during review:
- Switched to generative mechanism: TEST_F, GEN, which output during generation, and register during runtime.
- JS fixtures provide configuration members
- Add configuration hooks to generate in C++ test function
- Output directly to .cc file rather than needing hand-made .cc file which includes the generated file.
- Changed preload to take testFixture and testName.
- include and use mock4js to ensure handler methods are called.
- auto-generate the typedef WebUIBrowserTest testFixture unless overridden.

[email protected],[email protected]
BUG=None
TEST=browser_tests --gtest_filter=SettingsWebUITest.*

Review URL: http://codereview.chromium.org/7237030

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@92084 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/test/data/webui/options.js b/chrome/test/data/webui/options.js
new file mode 100644
index 0000000..d8a5089
--- /dev/null
+++ b/chrome/test/data/webui/options.js
@@ -0,0 +1,104 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * TestFixture for OptionsPage WebUI testing.
+ * @extends {testing.Test}
+ * @constructor
+ **/
+function OptionsWebUITest() {}
+
+OptionsWebUITest.prototype = {
+  __proto__: testing.Test.prototype,
+
+  /**
+   * Browse to the options page & call our PreLoad().
+   **/
+  browsePreload: 'chrome://settings',
+
+  /**
+   * Register a mock handler to ensure expectations are met and options pages
+   * behave correctly.
+   **/
+  PreLoad: function() {
+
+    /**
+     * Create handler class with empty methods to allow mocking to register
+     * expectations and for registration of handlers with chrome.send.
+     **/
+    function MockOptionsHandler() {}
+
+    MockOptionsHandler.prototype = {
+      coreOptionsInitialize: function() {},
+      fetchPrefs: function() {},
+      observePrefs: function() {},
+      setBooleanPref: function() {},
+      setIntegerPref: function() {},
+      setDoublePref: function() {},
+      setStringPref: function() {},
+      setObjectPref: function() {},
+      clearPref: function() {},
+      coreOptionsUserMetricsAction: function() {},
+    };
+
+    // Create the actual mock and register stubs for methods expected to be
+    // called before our tests run.  Specific expectations can be made in the
+    // tests themselves.
+    var mockHandler = this.mockHandler = mock(MockOptionsHandler);
+    mockHandler.stubs().fetchPrefs(ANYTHING);
+    mockHandler.stubs().observePrefs(ANYTHING);
+    mockHandler.stubs().coreOptionsInitialize();
+
+    // Register our mock as a handler of the chrome.send messages.
+    registerMockMessageCallbacks(mockHandler, MockOptionsHandler);
+  },
+};
+
+// Crashes on Mac only. See http://crbug.com/79181
+GEN('#if defined(OS_MACOSX)');
+GEN('#define MAYBE_testSetBooleanPrefTriggers ' +
+    'DISABLED_testSetBooleanPrefTriggers');
+GEN('#else');
+GEN('#define MAYBE_testSetBooleanPrefTriggers testSetBooleanPrefTriggers');
+GEN('#endif  // defined(OS_MACOSX)');
+
+TEST_F('OptionsWebUITest', 'MAYBE_testSetBooleanPrefTriggers', function() {
+  // TODO(dtseng): make generic to click all buttons.
+  var showHomeButton = $('toolbarShowHomeButton');
+  var trueListValue = [
+    'browser.show_home_button',
+    true,
+    'Options_Homepage_HomeButton',
+  ];
+  // Note: this expectation is checked in testing::Test::TearDown.
+  this.mockHandler.expects(once()).setBooleanPref(trueListValue);
+
+  // Cause the handler to be called.
+  showHomeButton.click();
+  showHomeButton.blur();
+});
+
+// Not meant to run on ChromeOS at this time.
+// Not finishing in windows. http://crbug.com/81723
+GEN('#if defined(OS_CHROMEOS) || defined(OS_MACOSX) || defined(OS_WIN) \\');
+GEN('    || defined(TOUCH_UI)');
+GEN('#define MAYBE_testRefreshStaysOnCurrentPage \\');
+GEN('    DISABLED_testRefreshStaysOnCurrentPage');
+GEN('#else');
+GEN('#define MAYBE_testRefreshStaysOnCurrentPage ' +
+    'testRefreshStaysOnCurrentPage');
+GEN('#endif');
+
+TEST_F('OptionsWebUITest', 'MAYBE_testRefreshStaysOnCurrentPage', function() {
+  var item = $('advancedPageNav');
+  item.onclick();
+  window.location.reload();
+  var pageInstance = AdvancedOptions.getInstance();
+  var topPage = OptionsPage.getTopmostVisiblePage();
+  var expectedTitle = pageInstance.title;
+  var actualTitle = document.title;
+  assertEquals("chrome://settings/advanced", document.location.href);
+  assertEquals(expectedTitle, actualTitle);
+  assertEquals(pageInstance, topPage);
+});
diff --git a/chrome/test/data/webui/print_preview.js b/chrome/test/data/webui/print_preview.js
index beb1c14..7b83977 100644
--- a/chrome/test/data/webui/print_preview.js
+++ b/chrome/test/data/webui/print_preview.js
@@ -2,85 +2,132 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-(function() {
-   function MockHandler() {
-     this.__proto__ = MockHandler.prototype;
-   };
+/**
+ * TestFixture for print preview WebUI testing.
+ * @extends {testing.Test}
+ * @constructor
+ **/
+function PrintPreviewWebUITest() {}
 
-   MockHandler.prototype = {
-     'getDefaultPrinter': function() {
-       console.log('getDefaultPrinter');
-       setDefaultPrinter('FooDevice');
-     },
-     'getPrinters': function() {
-       console.log('getPrinters');
-       setPrinters([
-                     {
-                       'printerName': 'FooName',
-                       'deviceName': 'FooDevice',
-                     },
-                     {
-                       'printerName': 'BarName',
-                       'deviceName': 'BarDevice',
-                     },
-                   ]);
-     },
-     'getPreview': function(settings) {
-       console.log('getPreview(' + settings + ')');
-       updatePrintPreview(1, 'title', true);
-     },
-     'print': function(settings) {
-       console.log('print(' + settings + ')');
-     },
-     'getPrinterCapabilities': function(printer_name) {
-       console.log('getPrinterCapabilities(' + printer_name + ')');
-       updateWithPrinterCapabilities({
-                                       'disableColorOption': true,
-                                       'setColorAsDefault': true,
-                                       'disableCopiesOption': true
-                                     });
-     },
-     'showSystemDialog': function() {
-       console.log('showSystemDialog');
-     },
-     'managePrinters': function() {
-       console.log('managePrinters');
-     },
-     'closePrintPreviewTab': function() {
-       console.log('closePrintPreviewTab');
-     },
-     'hidePreview': function() {
-       console.log('hidePreview');
-     },
-   };
+PrintPreviewWebUITest.prototype = {
+  __proto__: testing.Test.prototype,
 
-   function registerCallbacks() {
-     console.log('registeringCallbacks');
-     var mock_handler = new MockHandler();
-     for (func in MockHandler.prototype) {
-       if (typeof(mock_handler[func]) == 'function')
-         registerMessageCallback(func,
-                                 mock_handler,
-                                 mock_handler[func]);
-     }
-   };
+  /**
+   * Browse to the sample page, cause print preview & call our PreLoad().
+   **/
+  browsePrintPreload: 'print_preview_hello_world_test.html',
 
-   if ('window' in this && 'registerMessageCallback' in window)
-     registerCallbacks();
- })();
+  /**
+   * Register a mock handler to ensure expectations are met and print preview
+   * behaves correctly.
+   **/
+  PreLoad: function() {
 
-// Tests.
-function FLAKY_TestPrinterList() {
-  var printer_list = $('printer-list');
-  assertTrue(!!printer_list, 'printer_list');
-  assertTrue(printer_list.options.length >= 2, 'printer-list has at least 2');
-  expectEquals('FooName', printer_list.options[0].text, '0 text is FooName');
-  expectEquals('FooDevice', printer_list.options[0].value,
+    /**
+     * Create a handler class with empty methods to allow mocking to register
+     * expectations and for registration of handlers with chrome.send.
+     **/
+    function MockPrintPreviewHandler() {}
+
+    MockPrintPreviewHandler.prototype = {
+      getDefaultPrinter: function() {},
+      getPrinters: function() {},
+      getPreview: function(settings) {},
+      print: function(settings) {},
+      getPrinterCapabilities: function(printerName) {},
+      showSystemDialog: function() {},
+      managePrinters: function() {},
+      closePrintPreviewTab: function() {},
+      hidePreview: function() {},
+    };
+
+    // Create the actual mock and register stubs for methods expected to be
+    // called before our tests run.  Specific expectations can be made in the
+    // tests themselves.
+    var mockHandler = this.mockHandler = mock(MockPrintPreviewHandler);
+    mockHandler.stubs().getDefaultPrinter().
+        will(callFunction(function() {
+          setDefaultPrinter('FooDevice');
+        }));
+    mockHandler.stubs().getPrinterCapabilities(NOT_NULL).
+        will(callFunction(function() {
+          updateWithPrinterCapabilities({
+            disableColorOption: true,
+            setColorAsDefault: true,
+            disableCopiesOption: true,
+          });
+        }));
+    mockHandler.stubs().getPreview(NOT_NULL).
+        will(callFunction(function() {
+          updatePrintPreview(1, 'title', true);
+        }));
+
+    mockHandler.stubs().getPrinters().
+        will(callFunction(function() {
+          setPrinters([{
+              printerName: 'FooName',
+              deviceName: 'FooDevice',
+            }, {
+              printerName: 'BarName',
+              deviceName: 'BarDevice',
+            },
+          ]);
+        }));
+
+    // Register our mock as a handler of the chrome.send messages.
+    registerMockMessageCallbacks(mockHandler, MockPrintPreviewHandler);
+  },
+  testGenPreamble: function(testFixture, testName) {
+    GEN('  if (!HasPDFLib()) {');
+    GEN('    LOG(WARNING)');
+    GEN('        << "Skipping test ' + testFixture + '.' + testName + '"');
+    GEN('        << ": No PDF Lib.";');
+    GEN('    SUCCEED();');
+    GEN('    return;');
+    GEN('  }');
+  },
+  typedefCppFixture: null,
+};
+
+GEN('#include "base/command_line.h"');
+GEN('#include "base/path_service.h"');
+GEN('#include "base/stringprintf.h"');
+GEN('#include "chrome/browser/ui/webui/web_ui_browsertest.h"');
+GEN('#include "chrome/common/chrome_paths.h"');
+GEN('#include "chrome/common/chrome_switches.h"');
+GEN('');
+GEN('class PrintPreviewWebUITest');
+GEN('    : public WebUIBrowserTest {');
+GEN(' protected:');
+GEN('  // WebUIBrowserTest override.');
+GEN('  virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {');
+GEN('    WebUIBrowserTest::SetUpCommandLine(command_line);');
+GEN('#if !defined(GOOGLE_CHROME_BUILD) || defined(OS_CHROMEOS) || \\');
+GEN('    defined(OS_MACOSX)');
+GEN('    // Don\'t enable the flag for chrome builds, which should be on by ' +
+    'default.');
+GEN('    command_line->AppendSwitch(switches::kEnablePrintPreview);');
+GEN('#else');
+GEN('    ASSERT_TRUE(switches::IsPrintPreviewEnabled());');
+GEN('#endif');
+GEN('  }');
+GEN('');
+GEN('  bool HasPDFLib() const {');
+GEN('    FilePath pdf;');
+GEN('    return PathService::Get(chrome::FILE_PDF_PLUGIN, &pdf) &&');
+GEN('        file_util::PathExists(pdf);');
+GEN('  }');
+GEN('};');
+GEN('');
+
+TEST_F('PrintPreviewWebUITest', 'FLAKY_TestPrinterList', function() {
+  var printerList = $('printer-list');
+  assertTrue(!!printerList, 'printerList');
+  assertTrue(printerList.options.length >= 2, 'printer-list has at least 2');
+  expectEquals('FooName', printerList.options[0].text, '0 text is FooName');
+  expectEquals('FooDevice', printerList.options[0].value,
                '0 value is FooDevice');
-  expectEquals('BarName', printer_list.options[1].text, '1 text is BarName');
-  expectEquals('BarDevice', printer_list.options[1].value,
+  expectEquals('BarName', printerList.options[1].text, '1 text is BarName');
+  expectEquals('BarDevice', printerList.options[1].value,
                '1 value is BarDevice');
-}
-
-var test_fixture = 'PrintPreviewWebUITest';
-var test_add_library = false;
+});
diff --git a/chrome/test/data/webui/sample_pass.js b/chrome/test/data/webui/sample_pass.js
deleted file mode 100644
index 4e6e9fc..0000000
--- a/chrome/test/data/webui/sample_pass.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Sample tests that exercise the test JS library and show how this framework
-// could be used to test the downloads page.
-function testAssertFalse() {
-  assertFalse(false);
-}
-
-function testAssertTrue() {
-  assertTrue(true);
-}
-
-function testAssertEquals() {
-  assertEquals(5, 5, "fives");
-}
-
-var test_fixture = 'WebUIBrowserTestPass';
diff --git a/chrome/test/data/webui/settings.js b/chrome/test/data/webui/settings.js
deleted file mode 100644
index 2e44536..0000000
--- a/chrome/test/data/webui/settings.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Utility functions for settings WebUI page.
-function refreshPage() {
-  window.location.reload();
-}
-
-function openUnderTheHood() {
-  var item = $('advancedPageNav');
-  assertTrue(item != null);
-  assertTrue(item.onclick != null);
-  item.onclick();
-}
-
-// Tests.
-function testSetBooleanPrefTriggers() {
-  // TODO(dtseng): make generic to click all buttons.
-  var showHomeButton = $('toolbarShowHomeButton');
-  assertTrue(showHomeButton != null);
-  showHomeButton.click();
-  showHomeButton.blur();
-}
-
-function testPageIsUnderTheHood() {
-  var pageInstance = AdvancedOptions.getInstance();
-  var topPage = OptionsPage.getTopmostVisiblePage();
-  var expectedTitle = pageInstance.title;
-  var actualTitle = document.title;
-  assertEquals("chrome://settings/advanced", document.location.href);
-  assertEquals(expectedTitle, actualTitle);
-  assertEquals(pageInstance, topPage);
-}
diff --git a/chrome/test/data/webui/test_api.js b/chrome/test/data/webui/test_api.js
index c9dddc25..65e3f2f8 100644
--- a/chrome/test/data/webui/test_api.js
+++ b/chrome/test/data/webui/test_api.js
@@ -2,13 +2,233 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Library providing basic test framework functionality.
+/**
+ * @fileoverview Library providing basic test framework functionality.
+ **/
+
+/**
+ * Namespace for |Test|.
+ * @type {Object}
+ **/
+var testing = {};
+
+/**
+ * Hold the currentTestCase across between PreLoad and Run.
+ * @type {TestCase}
+ **/
+var currentTestCase = null;
 
 (function() {
+  // Provide global objects for generation case.
+  if (this['window'] === undefined)
+    this['window'] = this;
+  if (this['chrome'] === undefined) {
+    this['chrome'] = {
+      send: function() {},
+    };
+  }
+  if (this['console'] === undefined) {
+    this['console'] = {
+      log: print,
+    };
+  }
+
+  /**
+   * This class will be exported as testing.Test, and is provided to hold the
+   * fixture's configuration and callback methods for the various phases of
+   * invoking a test. It is called "Test" rather than TestFixture to roughly
+   * mimic the gtest's class names.
+   * @constructor
+   **/
+  function Test() {}
+
+  Test.prototype = {
+    /**
+     * The name of the test.
+     **/
+    name: null,
+
+    /**
+     * When set to a string value representing a url, generate BrowsePreload
+     * call, which will browse to the url and call fixture.PreLoad of the
+     * currentTestCase.
+     * @type {String}
+     **/
+    browsePreload: null,
+
+    /**
+     * When set to a string value representing an html page in the test
+     * directory, generate BrowsePrintPreload call, which will browse to a url
+     * representing the file, cause print, and call fixture.PreLoad of the
+     * currentTestCase.
+     * @type {String}
+     **/
+    browsePrintPreload: null,
+
+    /**
+     * When set to a function, will be called in the context of the test
+     * generation inside the function, and before any generated C++.
+     * @type {function(string,string)}
+     **/
+    testGenPreamble: null,
+
+    /**
+     * When set to a function, will be called in the context of the test
+     * generation inside the function, and before any generated C++.
+     * @type {function(string,string)}
+     **/
+    testGenPostamble: null,
+
+    /**
+     * When set to a non-null String, auto-generate typedef before generating
+     * TEST*: {@code typedef typedefCppFixture testFixture}.
+     * @type {String}
+     **/
+    typedefCppFixture: 'WebUIBrowserTest',
+
+    /**
+     * This should be initialized by the test fixture and can be referenced
+     * during the test run.
+     * @type {Mock4JS.Mock}
+     **/
+    mockHandler: null,
+
+    /**
+     * Override this method to perform initialization during preload (such as
+     * creating mocks and registering handlers).
+     * @type {Function}
+     **/
+    PreLoad: function() {},
+
+    /**
+     * Override this method to perform tasks before running your test.
+     * @type {Function}
+     **/
+    SetUp: function() {},
+
+    /**
+     * Override this method to perform tasks after running your test. If you
+     * create a mock class, you must call Mock4JS.verifyAllMocks() in this
+     * phase.
+     * @type {Function}
+     **/
+    TearDown: function() {
+      Mock4JS.verifyAllMocks();
+    }
+  };
+
+  /**
+   * This class is not exported and is available to hold the state of the
+   * |currentTestCase| throughout preload and test run.
+   * @param {String} name The name of the test case.
+   * @param {Test} fixture The fixture object for this test case.
+   * @param {Function} body The code to run for the test.
+   * @constructor
+   **/
+  function TestCase(name, fixture, body) {
+    this.name = name;
+    this.fixture = fixture;
+    this.body = body;
+  }
+
+  TestCase.prototype = {
+    name: null,
+    fixture: null,
+    body: null,
+
+    /**
+     * Called at preload time, proxies to the fixture.
+     * @type {Function}
+     **/
+    PreLoad: function(name) {
+      if (this.fixture)
+        this.fixture.PreLoad();
+    },
+
+    /**
+     * Runs this test case.
+     * @type {Function}
+     **/
+    Run: function() {
+      if (this.fixture)
+        this.fixture.SetUp();
+      if (this.body)
+        this.body.call(this.fixture);
+      if (this.fixture)
+        this.fixture.TearDown();
+    },
+  };
+
+  /**
+   * Registry of javascript-defined callbacks for {@code chrome.send}.
+   * @type {Object}
+   **/
+  var sendCallbacks = {};
+
+  /**
+   * Registers the message, object and callback for {@code chrome.send}
+   * @param {String} name The name of the message to route to this |callback|.
+   * @param {Object} messageHAndler Pass as |this| when calling the |callback|.
+   * @param {function(...)} callback Called by {@code chrome.send}.
+   * @see sendCallbacks
+   **/
+  function registerMessageCallback(name, messageHandler, callback) {
+    sendCallbacks[name] = [messageHandler, callback];
+  }
+
+  /**
+   * Register all methods of {@code mockClass.prototype} with messages of the
+   * same name as the method, using the proxy of the |mockObject| as the
+   * |messageHandler| when registering.
+   * @param {Mock4JS.Mock} mockObject The mock to register callbacks against.
+   * @param {function(new:Object)} mockClAss Constructor for the mocked class.
+   * @see registerMessageCallback
+   **/
+  function registerMockMessageCallbacks(mockObject, mockClass) {
+    var mockProxy = mockObject.proxy();
+    for (func in mockClass.prototype) {
+      if (typeof(mockClass.prototype[func]) == 'function') {
+        registerMessageCallback(func,
+                                mockProxy,
+                                mockProxy[func]);
+      }
+    }
+  }
+
+  /**
+   * Holds the old chrome object when overriding for preload and registry of
+   * handlers.
+   * @type {Object}
+   **/
+  var oldChrome = chrome;
+
+  /**
+   * Overrides {@code chrome.send} for routing messages to javascript
+   * functions. Also fallsback to sending with the |oldChrome| object.
+   * @param {String} messageName The message to route.
+   * @see oldChrome
+   **/
+  function send(messageName) {
+    var callback = sendCallbacks[messageName];
+    var args = Array.prototype.slice.call(arguments, 1);
+    if (callback != undefined)
+      callback[1].apply(callback[0], args);
+    else
+      oldChrome.send.apply(oldChrome, args);
+  }
+
   // Asserts.
   // Use the following assertions to verify a condition within a test.
   // If assertion fails, the C++ backend will be immediately notified.
   // If assertion passes, no notification will be sent to the C++ backend.
+
+  /**
+   * When |test| !== |expected|, aborts the current test.
+   * @param {Boolean} test The predicate to check against |expected|.
+   * @param {Boolean} expected The expected value of |test|.
+   * @param {String=} message The message to include in the Error thrown.
+   * @throws {Error} upon failure.
+   **/
   function assertBool(test, expected, message) {
     if (test !== expected) {
       if (message)
@@ -19,30 +239,33 @@
     }
   }
 
-  var old_chrome = chrome;
-  var send_callbacks = {};
-
-  function registerMessageCallback(name, object, callback) {
-    send_callbacks[name] = [object, callback];
-  }
-
-  function send(messageName) {
-    var callback = send_callbacks[messageName];
-    var args = Array.prototype.slice.call(arguments, 1);
-    if (callback != undefined)
-      callback[1].apply(callback[0], args);
-    else
-      old_chrome.send.apply(old_chrome, args);
-  }
-
+  /**
+   * When |test| !== true, aborts the current test.
+   * @param {Boolean} test The predicate to check against |expected|.
+   * @param {String=} message The message to include in the Error thrown.
+   * @throws {Error} upon failure.
+   **/
   function assertTrue(test, message) {
     assertBool(test, true, message);
   }
 
+  /**
+   * When |test| !== false, aborts the current test.
+   * @param {Boolean} test The predicate to check against |expected|.
+   * @param {String=} message The message to include in the Error thrown.
+   * @throws {Error} upon failure.
+   **/
   function assertFalse(test, message) {
     assertBool(test, false, message);
   }
 
+  /**
+   * When |expected| !== |actual|, aborts the current test.
+   * @param {*} expected The predicate to check against |expected|.
+   * @param {*} actual The expected value of |test|.
+   * @param {String=} message The message to include in the Error thrown.
+   * @throws {Error} upon failure.
+   **/
   function assertEquals(expected, actual, message) {
     if (expected != actual) {
       throw new Error('Test Error. Actual: ' + actual + '\nExpected: ' +
@@ -55,56 +278,220 @@
     }
   }
 
+  /**
+   * Always aborts the current test.
+   * @param {String=} message The message to include in the Error thrown.
+   * @throws {Error} always.
+   **/
   function assertNotReached(message) {
     throw new Error(message);
   }
 
+  /**
+   * Holds the errors, if any, caught by expects so that the test case can fail.
+   * @type {Array.<Error>}
+   **/
   var errors = [];
 
+  /**
+   * Creates a function based upon a function that thows an exception on
+   * failure. The new function stuffs any errors into the |errors| array for
+   * checking by runTest. This allows tests to continue running other checks,
+   * while failing the overal test if any errors occurrred.
+   * @param {Function} assertFunc The function which may throw an Error.
+   * @return {Function} A function that applies its arguments to |assertFunc|.
+   * @see errors
+   * @see runTest
+   **/
   function createExpect(assertFunc) {
     return function() {
       try {
         assertFunc.apply(null, arguments);
       } catch (e) {
-        console.log('Failed: ' + currentTest.name + '\n' + e.stack);
         errors.push(e);
       }
     };
   }
 
+  /**
+   * This is the starting point for tests run by WebUIBrowserTest. It clears
+   * |errors|, runs the test surrounded by an expect to catch Errors. If
+   * |errors| is non-empty, it reports a failure and a message by joining
+   * |errors|.
+   * @param {String} testFunction The function name to call.
+   * @param {Array} testArguments The arguments to call |testFunction| with.
+   * @return {Array.<Boolean, String>} [test-succeeded, message-if-failed]
+   * @see errors
+   * @see createExpect
+   **/
   function runTest(testFunction, testArguments) {
     errors = [];
     // Avoid eval() if at all possible, since it will not work on pages
     // that have enabled content-security-policy.
-    currentTest = this[testFunction];    // global object -- not a method.
-    if (typeof currentTest === "undefined") {
-      currentTest = eval(testFunction);
-    }
-    console.log('Running test ' + currentTest.name);
-    createExpect(currentTest).apply(null, testArguments);
+    var testBody = this[testFunction];    // global object -- not a method.
+    if (typeof testBody === "undefined")
+      testBody = eval(testFunction);
+    if (testBody != RUN_TEST_F)
+      console.log('Running test ' + testBody.name);
+    createExpect(testBody).apply(null, testArguments);
 
     if (errors.length) {
+      for (var i = 0; i < errors.length; ++i) {
+        console.log('Failed: ' + testFunction + '(' +
+                    testArguments.toString() + ')\n' + errors[i].stack);
+      }
       return [false, errors.join('\n')];
+    } else {
+      return [true];
     }
-
-    return [true];
   }
 
-  function preloadJavascriptLibraries(overload_chrome_send) {
-    if (overload_chrome_send)
-      chrome = { 'send': send };
+  /**
+   * Creates a new test case for the given |testFixture| and |testName|. Assumes
+   * |testFixture| describes a globally available subclass of type Test.
+   * @param {String} testFixture The fixture for this test case.
+   * @param {String} testName The name for this test case.
+   * @return {TestCase} A newly created TestCase.
+   **/
+  function createTestCase(testFixture, testName) {
+    var fixtureConstructor = this[testFixture];
+    var testBody = fixtureConstructor.testCaseBodies[testName];
+    var fixture = new fixtureConstructor();
+    fixture.name = testFixture;
+    return new TestCase(testName, fixture, testBody);
+  }
+
+  /**
+   * Used by WebUIBrowserTest to preload the javascript libraries at the
+   * appropriate time for javascript injection into the current page. This
+   * creates a test case and calls its PreLoad for any early initialization such
+   * as registering handlers before the page's javascript runs it's OnLoad
+   * method.
+   * @param {String} testFixture The test fixture name.
+   * @param {String} testName The test name.
+   **/
+  function preloadJavascriptLibraries(testFixture, testName) {
+    chrome = {
+      __proto__: oldChrome,
+      send: send,
+    };
+    currentTestCase = createTestCase(testFixture, testName);
+    currentTestCase.PreLoad();
+  }
+
+  /**
+   * During generation phase, this outputs; do nothing at runtime.
+   **/
+  function GEN() {}
+
+  /**
+   * At runtime, register the testName with a test fixture. Since this method
+   * doesn't have a test fixture, we create a dummy fixture to hold its |name|
+   * and |testCaseBodies|.
+   * @param {String} testCaseName The name of the test case.
+   * @param {String} testName The name of the test function.
+   * @param {Function} testBody The body to execute when running this test.
+   **/
+  function TEST(testCaseName, testName, testBody) {
+    var fixtureConstructor = this[testCaseName];
+    if (fixtureConstructor === undefined) {
+      fixtureConstructor = function() {};
+      this[testCaseName] = fixtureConstructor;
+      fixtureConstructor.prototype = {
+        __proto__: Test.prototype,
+        name: testCaseName,
+      };
+      fixtureConstructor.testCaseBodies = {};
+    }
+    fixtureConstructor.testCaseBodies[testName] = testBody;
+  }
+
+  /**
+   * At runtime, register the testName with its fixture. Stuff the |name| into
+   * the |testFixture|'s prototype, if needed, and the |testCaseBodies| into its
+   * constructor.
+   * @param {String} testFixture The name of the test fixture class.
+   * @param {String} testName The name of the test function.
+   * @param {Function} testBody The body to execute when running this test.
+   **/
+  function TEST_F(testFixture, testName, testBody) {
+    var fixtureConstructor = this[testFixture];
+    if (!fixtureConstructor.prototype.name)
+      fixtureConstructor.prototype.name = testFixture;
+    if (fixtureConstructor['testCaseBodies'] === undefined)
+      fixtureConstructor.testCaseBodies = {};
+    fixtureConstructor.testCaseBodies[testName] = testBody;
+  }
+
+  /**
+   * RunJavascriptTestF uses this as the |testFunction| when invoking
+   * runTest. If |currentTestCase| is non-null at this point, verify that
+   * |testFixture| and |testName| agree with the preloaded values. Create
+   * |currentTestCase|, if needed, run it, and clear the |currentTestCase|.
+   * @param {String} testFixture The name of the test fixture class.
+   * @param {String} testName The name of the test function.
+   * @see preloadJavascriptLibraries
+   * @see runTest
+   **/
+  function RUN_TEST_F(testFixture, testName) {
+    if (!currentTestCase)
+      currentTestCase = createTestCase(testFixture, testName);
+    assertEquals(currentTestCase.name, testName);
+    assertEquals(currentTestCase.fixture.name, testFixture);
+    console.log('Running TestCase ' + testFixture + '.' + testName);
+    currentTestCase.Run();
+    currentTestCase = null;
+  }
+
+  /**
+   * CallFunctionAction is provided to allow mocks to have side effects.
+   * @param {Function} func The function to call.
+   * @param {Array} args Any arguments to pass to func.
+   * @constructor
+   **/
+  function CallFunctionAction(func, args) {
+    this._func = func;
+    this._args = args;
+  }
+
+  CallFunctionAction.prototype = {
+    invoke: function() {
+      return this._func.apply(null, this._args);
+    },
+    describe: function() {
+      return 'calls the given function with arguments ' + this._args;
+    }
+  };
+
+  /**
+   * Syntactic sugar for will() on a Mock4JS.Mock.
+   * @param {Function} func the function to call when the method is invoked.
+   * @param {...*} var_args arguments to pass when calling func.
+   **/
+  function callFunction(func) {
+    return new CallFunctionAction(func,
+                                  Array.prototype.slice.call(arguments, 1));
   }
 
   // Exports.
+  testing.Test = Test;
   window.assertTrue = assertTrue;
   window.assertFalse = assertFalse;
   window.assertEquals = assertEquals;
   window.assertNotReached = assertNotReached;
+  window.callFunction = callFunction;
   window.expectTrue = createExpect(assertTrue);
   window.expectFalse = createExpect(assertFalse);
   window.expectEquals = createExpect(assertEquals);
   window.expectNotReached = createExpect(assertNotReached);
   window.registerMessageCallback = registerMessageCallback;
+  window.registerMockMessageCallbacks = registerMockMessageCallbacks;
   window.runTest = runTest;
   window.preloadJavascriptLibraries = preloadJavascriptLibraries;
+  window.TEST = TEST;
+  window.TEST_F = TEST_F;
+  window.GEN = GEN;
+
+  // Import the Mock4JS helpers.
+  Mock4JS.addMockSupport(window);
 })();
diff --git a/chrome/test/test_navigation_observer.cc b/chrome/test/test_navigation_observer.cc
new file mode 100644
index 0000000..b7bcc74
--- /dev/null
+++ b/chrome/test/test_navigation_observer.cc
@@ -0,0 +1,83 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/test/test_navigation_observer.h"
+
+#include "chrome/test/ui_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+TestNavigationObserver::JsInjectionReadyObserver::JsInjectionReadyObserver() {
+}
+
+TestNavigationObserver::JsInjectionReadyObserver::~JsInjectionReadyObserver() {
+}
+
+TestNavigationObserver::TestNavigationObserver(
+    NavigationController* controller,
+    TestNavigationObserver::JsInjectionReadyObserver*
+        js_injection_ready_observer,
+    int number_of_navigations)
+    : navigation_started_(false),
+      navigation_entry_committed_(false),
+      navigations_completed_(0),
+      number_of_navigations_(number_of_navigations),
+      js_injection_ready_observer_(js_injection_ready_observer),
+      done_(false),
+      running_(false) {
+  RegisterAsObserver(controller);
+}
+
+TestNavigationObserver::TestNavigationObserver(
+    TestNavigationObserver::JsInjectionReadyObserver*
+        js_injection_ready_observer,
+    int number_of_navigations)
+    : navigation_started_(false),
+      navigations_completed_(0),
+      number_of_navigations_(number_of_navigations),
+      js_injection_ready_observer_(js_injection_ready_observer),
+      done_(false),
+      running_(false) {
+}
+
+TestNavigationObserver::~TestNavigationObserver() {
+}
+
+void TestNavigationObserver::RegisterAsObserver(
+    NavigationController* controller) {
+  registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
+                 Source<NavigationController>(controller));
+  registrar_.Add(this, content::NOTIFICATION_LOAD_START,
+                 Source<NavigationController>(controller));
+  registrar_.Add(this, content::NOTIFICATION_LOAD_STOP,
+                 Source<NavigationController>(controller));
+}
+
+void TestNavigationObserver::WaitForObservation() {
+  if (!done_) {
+    EXPECT_FALSE(running_);
+    running_ = true;
+    ui_test_utils::RunMessageLoop();
+  }
+}
+
+void TestNavigationObserver::Observe(
+    int type, const NotificationSource& source,
+    const NotificationDetails& details) {
+  if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) {
+    if (!navigation_entry_committed_ && js_injection_ready_observer_)
+      js_injection_ready_observer_->OnJsInjectionReady();
+    navigation_started_ = true;
+    navigation_entry_committed_ = true;
+  } else if (type == content::NOTIFICATION_LOAD_START) {
+    navigation_started_ = true;
+  } else if (type == content::NOTIFICATION_LOAD_STOP) {
+    if (navigation_started_ &&
+        ++navigations_completed_ == number_of_navigations_) {
+      navigation_started_ = false;
+      done_ = true;
+      if (running_)
+        MessageLoopForUI::current()->Quit();
+    }
+  }
+}
diff --git a/chrome/test/test_navigation_observer.h b/chrome/test/test_navigation_observer.h
new file mode 100644
index 0000000..ba76ab5a
--- /dev/null
+++ b/chrome/test/test_navigation_observer.h
@@ -0,0 +1,90 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_TEST_TEST_NAVIGATION_OBSERVER_H_
+#define CHROME_TEST_TEST_NAVIGATION_OBSERVER_H_
+#pragma once
+
+#include "base/compiler_specific.h"
+#include "content/common/notification_observer.h"
+#include "content/common/notification_registrar.h"
+
+class NavigationController;
+
+// In order to support testing of print preview, we need to wait for the tab to
+// be inserted, and then observe notifications on the newly added tab's
+// controller to wait for it to be loaded. To support tests registering
+// javascript WebUI handlers, we need to inject the framework & registration
+// javascript before the webui page loads by calling back through the
+// TestTabStripModelObserver::LoadStartObserver when the new page starts
+// loading.
+class TestNavigationObserver : public NotificationObserver {
+ public:
+  class JsInjectionReadyObserver {
+   public:
+    JsInjectionReadyObserver();
+    virtual ~JsInjectionReadyObserver();
+
+    // Called to indicate page entry committed and ready for javascript
+    // injection.
+    virtual void OnJsInjectionReady() = 0;
+  };
+
+  // Create and register a new TestNavigationObserver against the
+  // |controller|. When |js_injection_ready_observer| is non-null, notify with
+  // OnEntryCommitted() after |number_of_navigations| navigations.
+  // Note: |js_injection_ready_observer| is owned by the caller and should be
+  // valid until this class is destroyed.
+  TestNavigationObserver(NavigationController* controller,
+                         JsInjectionReadyObserver* js_injection_ready_observer,
+                         int number_of_navigations);
+
+  virtual ~TestNavigationObserver();
+
+  // Run the UI message loop until |done_| becomes true.
+  void WaitForObservation();
+
+ protected:
+  // Note: |js_injection_ready_observer| is owned by the caller and should be
+  // valid until this class is destroyed.
+  explicit TestNavigationObserver(
+      JsInjectionReadyObserver* js_injection_ready_observer,
+      int number_of_navigations);
+
+  // Register this TestNavigationObserver as an observer of the |controller|.
+  void RegisterAsObserver(NavigationController* controller);
+
+ private:
+  // NotificationObserver:
+  virtual void Observe(int type, const NotificationSource& source,
+                       const NotificationDetails& details) OVERRIDE;
+
+  NotificationRegistrar registrar_;
+
+  // If true the navigation has started.
+  bool navigation_started_;
+
+  // If true the navigation has been committed.
+  bool navigation_entry_committed_;
+
+  // The number of navigations that have been completed.
+  int navigations_completed_;
+
+  // The number of navigations to wait for.
+  int number_of_navigations_;
+
+  // Observer to take some action when the page is ready for javascript
+  // injection.
+  JsInjectionReadyObserver* js_injection_ready_observer_;
+
+  // |done_| will get set when this object observes a TabStripModel event.
+  bool done_;
+
+  // |running_| will be true during WaitForObservation until |done_| is true.
+  bool running_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestNavigationObserver);
+};
+
+#endif  // CHROME_TEST_TEST_NAVIGATION_OBSERVER_H_
diff --git a/chrome/test/test_tab_strip_model_observer.cc b/chrome/test/test_tab_strip_model_observer.cc
index 9a209c6..013e2ef5 100644
--- a/chrome/test/test_tab_strip_model_observer.cc
+++ b/chrome/test/test_tab_strip_model_observer.cc
@@ -6,25 +6,13 @@
 
 #include "chrome/browser/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
-#include "chrome/test/ui_test_utils.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-TestTabStripModelObserver::LoadStartObserver::LoadStartObserver() {
-}
-
-TestTabStripModelObserver::LoadStartObserver::~LoadStartObserver() {
-}
 
 TestTabStripModelObserver::TestTabStripModelObserver(
     TabStripModel* tab_strip_model,
-    TestTabStripModelObserver::LoadStartObserver* load_start_observer)
-    : navigation_started_(false),
-      navigations_completed_(0),
-      number_of_navigations_(1),
-      tab_strip_model_(tab_strip_model),
-      load_start_observer_(load_start_observer),
-      done_(false),
-      running_(false) {
+    TestTabStripModelObserver::JsInjectionReadyObserver*
+        js_injection_ready_observer)
+    : TestNavigationObserver(js_injection_ready_observer, 1),
+      tab_strip_model_(tab_strip_model) {
   tab_strip_model_->AddObserver(this);
 }
 
@@ -32,41 +20,7 @@
   tab_strip_model_->RemoveObserver(this);
 }
 
-void TestTabStripModelObserver::WaitForObservation() {
-  if (!done_) {
-    EXPECT_FALSE(running_);
-    running_ = true;
-    ui_test_utils::RunMessageLoop();
-  }
-}
-
 void TestTabStripModelObserver::TabInsertedAt(
     TabContentsWrapper* contents, int index, bool foreground) {
-  NavigationController* controller = &contents->controller();
-  registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
-                 Source<NavigationController>(controller));
-  registrar_.Add(this, content::NOTIFICATION_LOAD_START,
-                 Source<NavigationController>(controller));
-  registrar_.Add(this, content::NOTIFICATION_LOAD_STOP,
-                 Source<NavigationController>(controller));
-}
-
-void TestTabStripModelObserver::Observe(
-    int type, const NotificationSource& source,
-    const NotificationDetails& details) {
-  if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED ||
-      type == content::NOTIFICATION_LOAD_START) {
-    if (!navigation_started_) {
-      load_start_observer_->OnLoadStart();
-      navigation_started_ = true;
-    }
-  } else if (type == content::NOTIFICATION_LOAD_STOP) {
-    if (navigation_started_ &&
-        ++navigations_completed_ == number_of_navigations_) {
-      navigation_started_ = false;
-      done_ = true;
-      if (running_)
-        MessageLoopForUI::current()->Quit();
-    }
-  }
+  RegisterAsObserver(&contents->controller());
 }
diff --git a/chrome/test/test_tab_strip_model_observer.h b/chrome/test/test_tab_strip_model_observer.h
index c384e45..39890a92 100644
--- a/chrome/test/test_tab_strip_model_observer.h
+++ b/chrome/test/test_tab_strip_model_observer.h
@@ -8,8 +8,7 @@
 
 #include "base/compiler_specific.h"
 #include "chrome/browser/tabs/tab_strip_model_observer.h"
-#include "content/common/notification_observer.h"
-#include "content/common/notification_registrar.h"
+#include "chrome/test/test_navigation_observer.h"
 
 class TabStripModel;
 
@@ -20,60 +19,25 @@
 // javascript before the webui page loads by calling back through the
 // TestTabStripModelObserver::LoadStartObserver when the new page starts
 // loading.
-class TestTabStripModelObserver : public TabStripModelObserver,
-                                  public NotificationObserver {
+class TestTabStripModelObserver : public TestNavigationObserver,
+                                  public TabStripModelObserver {
  public:
-  class LoadStartObserver {
-   public:
-    LoadStartObserver();
-    virtual ~LoadStartObserver();
-
-    // Called to indicate page load starting.
-    virtual void OnLoadStart() = 0;
-  };
-
   // Observe the |tab_strip_model|, which may not be NULL. If
   // |load_start_observer| is non-NULL, notify when the page load starts.
-  TestTabStripModelObserver(TabStripModel* tab_strip_model,
-                            LoadStartObserver* load_start_observer);
+  TestTabStripModelObserver(
+      TabStripModel* tab_strip_model,
+      JsInjectionReadyObserver* js_injection_ready_observer);
   virtual ~TestTabStripModelObserver();
 
-  // Run the UI message loop until |done_| becomes true.
-  void WaitForObservation();
-
  private:
   // TabStripModelObserver:
   virtual void TabInsertedAt(TabContentsWrapper* contents, int index,
                              bool foreground) OVERRIDE;
 
-  // NotificationObserver:
-  virtual void Observe(int type, const NotificationSource& source,
-                       const NotificationDetails& details) OVERRIDE;
-
-  NotificationRegistrar registrar_;
-
-  // If true the navigation has started.
-  bool navigation_started_;
-
-  // The number of navigations that have been completed.
-  int navigations_completed_;
-
-  // The number of navigations to wait for.
-  int number_of_navigations_;
-
   // |tab_strip_model_| is the object this observes. The constructor will
   // register this as an observer, and the destructor will remove the observer.
   TabStripModel* tab_strip_model_;
 
-  // Observer to take some action when the page load starts.
-  LoadStartObserver* load_start_observer_;
-
-  // |done_| will get set when this object observes a TabStripModel event.
-  bool done_;
-
-  // |running_| will be true during WaitForObservation until |done_| is true.
-  bool running_;
-
   DISALLOW_COPY_AND_ASSIGN(TestTabStripModelObserver);
 };
 
diff --git a/chrome/test/ui_test_utils.cc b/chrome/test/ui_test_utils.cc
index 8ed430348..d8f8c75 100644
--- a/chrome/test/ui_test_utils.cc
+++ b/chrome/test/ui_test_utils.cc
@@ -35,6 +35,7 @@
 #include "chrome/common/extensions/extension_action.h"
 #include "chrome/test/automation/javascript_execution_controller.h"
 #include "chrome/test/bookmark_load_observer.h"
+#include "chrome/test/test_navigation_observer.h"
 #include "content/browser/renderer_host/render_process_host.h"
 #include "content/browser/renderer_host/render_view_host.h"
 #include "content/browser/tab_contents/navigation_controller.h"
@@ -55,70 +56,6 @@
 
 namespace {
 
-// Used to block until a navigation completes.
-class NavigationNotificationObserver : public NotificationObserver {
- public:
-  NavigationNotificationObserver(NavigationController* controller,
-                                 int number_of_navigations)
-      : navigation_started_(false),
-        navigations_completed_(0),
-        number_of_navigations_(number_of_navigations),
-        running_(false),
-        done_(false) {
-    registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
-                   Source<NavigationController>(controller));
-    registrar_.Add(this, content::NOTIFICATION_LOAD_START,
-                   Source<NavigationController>(controller));
-    registrar_.Add(this, content::NOTIFICATION_LOAD_STOP,
-                   Source<NavigationController>(controller));
-  }
-
-  void Run() {
-    if (!done_) {
-      running_ = true;
-      RunMessageLoop();
-    }
-  }
-
-  virtual void Observe(int type,
-                       const NotificationSource& source,
-                       const NotificationDetails& details) {
-    if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED ||
-        type == content::NOTIFICATION_LOAD_START) {
-      navigation_started_ = true;
-    } else if (type == content::NOTIFICATION_LOAD_STOP) {
-      if (navigation_started_ &&
-          ++navigations_completed_ == number_of_navigations_) {
-        navigation_started_ = false;
-        done_ = true;
-        if (running_)
-          MessageLoopForUI::current()->Quit();
-      }
-    }
-  }
-
- private:
-  NotificationRegistrar registrar_;
-
-  // If true the navigation has started.
-  bool navigation_started_;
-
-  // The number of navigations that have been completed.
-  int navigations_completed_;
-
-  // The number of navigations to wait for.
-  int number_of_navigations_;
-
-  // Calls to Observe() can happen early, before the user calls Run(), or
-  // after.  When we've seen all the navigations we're looking for, we set
-  // done_ to true; then when Run() is called we'll never need to run the
-  // event loop.  Also, we don't need to quit the event loop when we're
-  // done if we never had to start an event loop.
-  bool running_;
-  bool done_;
-  DISALLOW_COPY_AND_ASSIGN(NavigationNotificationObserver);
-};
-
 class DOMOperationObserver : public NotificationObserver {
  public:
   explicit DOMOperationObserver(RenderViewHost* render_view_host)
@@ -337,8 +274,8 @@
 
 void WaitForNavigations(NavigationController* controller,
                         int number_of_navigations) {
-  NavigationNotificationObserver observer(controller, number_of_navigations);
-  observer.Run();
+  TestNavigationObserver observer(controller, NULL, number_of_navigations);
+  observer.WaitForObservation();
 }
 
 void WaitForNewTab(Browser* browser) {
@@ -404,9 +341,9 @@
     int number_of_navigations,
     WindowOpenDisposition disposition,
     int browser_test_flags) {
-  NavigationNotificationObserver
+  TestNavigationObserver
       same_tab_observer(&browser->GetSelectedTabContents()->controller(),
-                        number_of_navigations);
+                        NULL, number_of_navigations);
 
   std::set<Browser*> initial_browsers;
   for (std::vector<Browser*>::const_iterator iter = BrowserList::begin();
@@ -438,7 +375,7 @@
     tab_contents = browser->GetSelectedTabContents();
   }
   if (disposition == CURRENT_TAB) {
-    same_tab_observer.Run();
+    same_tab_observer.WaitForObservation();
     return;
   } else if (tab_contents) {
     NavigationController* controller = &tab_contents->controller();