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();