diff options
author | erikwright@chromium.org <erikwright@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-21 19:03:33 +0000 |
---|---|---|
committer | erikwright@chromium.org <erikwright@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-21 19:03:33 +0000 |
commit | 0bf46b99bb79d66c81587cb114b93516cb0af499 (patch) | |
tree | a7220ed87219b47ce247412d50551ff385d87f53 /chrome_frame | |
parent | 102907fa8772f074bfd0f2b48bcdf75a819b6906 (diff) | |
download | chromium_src-0bf46b99bb79d66c81587cb114b93516cb0af499.zip chromium_src-0bf46b99bb79d66c81587cb114b93516cb0af499.tar.gz chromium_src-0bf46b99bb79d66c81587cb114b93516cb0af499.tar.bz2 |
A test for Chrome Frame delete browser history behaviour in IE8.
BUG=52612
TEST=chrome_frame_tests --gtest_filter=DeleteBrowsingHistoryTest.*
Review URL: http://codereview.chromium.org/3365010/show
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@60084 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome_frame')
-rw-r--r-- | chrome_frame/chrome_frame.gyp | 25 | ||||
-rw-r--r-- | chrome_frame/test/delete_chrome_history_test.cc | 250 | ||||
-rw-r--r-- | chrome_frame/test/mock_ie_event_sink_actions.h | 67 | ||||
-rw-r--r-- | chrome_frame/test/mock_ie_event_sink_test.h | 54 | ||||
-rw-r--r-- | chrome_frame/test/navigation_test.cc | 32 | ||||
-rw-r--r-- | chrome_frame/test/net/fake_external_tab.cc | 13 | ||||
-rw-r--r-- | chrome_frame/test/ui_test.cc | 67 | ||||
-rw-r--r-- | chrome_frame/test/win_event_receiver.cc | 191 | ||||
-rw-r--r-- | chrome_frame/test/win_event_receiver.h | 78 |
9 files changed, 649 insertions, 128 deletions
diff --git a/chrome_frame/chrome_frame.gyp b/chrome_frame/chrome_frame.gyp index 3560f7f..8f3d62e 100644 --- a/chrome_frame/chrome_frame.gyp +++ b/chrome_frame/chrome_frame.gyp @@ -226,6 +226,9 @@ '../build/temp_gyp/googleurl.gyp:googleurl', '../chrome/chrome.gyp:common', '../chrome/chrome.gyp:utility', + '../chrome/chrome.gyp:browser', + '../chrome/chrome.gyp:debugger', + '../chrome/chrome.gyp:renderer', '../net/net.gyp:net_test_support', '../testing/gmock.gyp:gmock', '../testing/gtest.gyp:gtest', @@ -233,7 +236,7 @@ 'chrome_frame_ie', 'chrome_frame_npapi', 'chrome_frame_strings', - 'npchrome_frame', + 'npchrome_frame', 'xulrunner_sdk', ], 'sources': [ @@ -246,6 +249,7 @@ 'test/chrome_frame_ui_test_utils.h', 'test/chrome_frame_automation_mock.cc', 'test/chrome_frame_automation_mock.h', + 'test/delete_chrome_history_test.cc', 'test/http_server.cc', 'test/http_server.h', 'test/ie_event_sink.cc', @@ -272,7 +276,6 @@ 'test/win_event_receiver.h', 'chrome_tab.h', 'chrome_tab.idl', - 'renderer_glue.cc', 'test_utils.cc', 'test_utils.h', ], @@ -302,7 +305,23 @@ '../chrome/chrome.gyp:automation', '../chrome/chrome.gyp:installer_util', '../google_update/google_update.gyp:google_update', - ] + ], + 'configurations': { + 'Debug_Base': { + 'msvs_settings': { + 'VCLinkerTool': { + 'LinkIncremental': '<(msvs_large_module_debug_link_mode)', + }, + }, + }, + }, + 'conditions': [ + ['win_use_allocator_shim==1', { + 'dependencies': [ + '../base/allocator/allocator.gyp:allocator', + ], + }], + ], }], ], }, diff --git a/chrome_frame/test/delete_chrome_history_test.cc b/chrome_frame/test/delete_chrome_history_test.cc new file mode 100644 index 0000000..5f317f7 --- /dev/null +++ b/chrome_frame/test/delete_chrome_history_test.cc @@ -0,0 +1,250 @@ +// Copyright (c) 2010 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 <string> + +#include "base/rand_util.h" +#include "chrome/browser/webdata/web_database.h" +#include "chrome/common/url_constants.h" +#include "chrome/common/chrome_constants.h" +#include "chrome_frame/test/mock_ie_event_sink_actions.h" +#include "chrome_frame/test/mock_ie_event_sink_test.h" + +using testing::_; + +namespace chrome_frame_test { + +class DeleteBrowsingHistoryTest + : public MockIEEventSinkTest, + public testing::Test { + public: + DeleteBrowsingHistoryTest() {} + + virtual void SetUp() { + // We will use the OnAccLoad event to monitor page loads, so we ignore + // these. + ie_mock_.ExpectAnyNavigations(); + ie_mock2_.ExpectAnyNavigations(); + ie_mock3_.ExpectAnyNavigations(); + EXPECT_CALL(ie_mock_, OnLoad(_, _)).Times(testing::AnyNumber()); + EXPECT_CALL(ie_mock2_, OnLoad(_, _)).Times(testing::AnyNumber()); + EXPECT_CALL(ie_mock3_, OnLoad(_, _)).Times(testing::AnyNumber()); + + // Use a random image_path to ensure that a prior run does not + // interfere with our expectations about caching. + image_path_ = L"/" + RandomChars(32); + topHtml = + "<html><head>" + "<meta http-equiv=\"x-ua-compatible\" content=\"chrome=1\" />" + "</head>" + "<body>" + "<form method=\"POST\" action=\"/form\">" + "<input title=\"username\" type=\"text\" name=\"username\" />" + "<input type=\"submit\" title=\"Submit\" name=\"Submit\" />" + "</form>" + "<img alt=\"Blank image.\" src=\"" + WideToASCII(image_path_) + "\" />" + "This is some text.</body></html>"; + } + + protected: + std::wstring image_path_; + std::string topHtml; + + testing::NiceMock<MockAccEventObserver> acc_observer_; + MockWindowObserver delete_browsing_history_window_observer_mock_; + MockObjectWatcherDelegate ie_process_exit_watcher_mock_; + + testing::StrictMock<MockIEEventSink> ie_mock2_; + testing::StrictMock<MockIEEventSink> ie_mock3_; + + private: + // Returns a string of |count| lowercase random characters. + static std::wstring RandomChars(int count) { + srand(static_cast<unsigned int>(time(NULL))); + std::wstring str; + for (int i = 0; i < count; ++i) + str += L'a' + base::RandInt(0, 25); + return str; + } +}; + +namespace { + +const wchar_t* kFormFieldName = L"username"; +const wchar_t* kFormFieldValue = L"test_username"; + +const char* kHtmlHttpHeaders = + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Type: text/html\r\n"; +const char* kFormResultHtml = + "<html><head><meta http-equiv=\"x-ua-compatible\" content=\"chrome=1\" />" + "</head><body>Nice work.</body></html>"; +const char* kBlankPngResponse[] = { + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Type: image/png\r\n" + "Cache-Control: max-age=3600, must-revalidate\r\n", + "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" + "\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00\x25\xdb\x56" + "\xca\x00\x00\x00\x03\x50\x4c\x54\x45\x00\x00\x00\xa7\x7a\x3d\xda" + "\x00\x00\x00\x01\x74\x52\x4e\x53\x00\x40\xe6\xd8\x66\x00\x00\x00" + "\x0a\x49\x44\x41\x54\x08\xd7\x63\x60\x00\x00\x00\x02\x00\x01\xe2" + "\x21\xbc\x33\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82"}; + +const size_t kBlankPngFileLength = 95; +} // anonymous namespace + +// Looks up |element_name| in the Chrome form data DB and ensures that the +// results match |matcher|. +ACTION_P2(ExpectFormValuesForElementNameMatch, element_name, matcher) { + FilePath profile_path( + chrome_frame_test::GetProfilePath(kIexploreProfileName) + .Append(L"Default").Append(chrome::kWebDataFilename)); + + WebDatabase web_database; + sql::InitStatus init_status = web_database.Init(profile_path); + EXPECT_EQ(sql::INIT_OK, init_status); + + if (init_status == sql::INIT_OK) { + std::vector<string16> values; + web_database.GetFormValuesForElementName(element_name, L"", &values, 9999); + EXPECT_THAT(values, matcher); + } +} + +// Launch |ie_mock| and navigate it to |url|. +ACTION_P2(LaunchThisIEAndNavigate, ie_mock, url) { + EXPECT_HRESULT_SUCCEEDED(ie_mock->event_sink()->LaunchIEAndNavigate(url, + ie_mock)); +} + +TEST_F(DeleteBrowsingHistoryTest, CFDeleteBrowsingHistory) { + if (GetInstalledIEVersion() < IE_8) { + LOG(ERROR) << "Test does not apply to IE versions < 8."; + return; + } + delete_browsing_history_window_observer_mock_.WatchWindow( + "Delete Browsing History"); + + EXPECT_CALL(server_mock_, Get(_, testing::StrEq(L"/top.html"), _)) + .WillRepeatedly(SendFast(kHtmlHttpHeaders, topHtml)); + EXPECT_CALL(server_mock_, Post(_, testing::StrEq(L"/form"), _)) + .WillRepeatedly(SendFast(kHtmlHttpHeaders, kFormResultHtml)); + + std::wstring top_url = server_mock_.Resolve(L"top.html"); + + testing::InSequence expect_in_sequence_for_scope; + + // First launch will hit the server, requesting top.html and then image_path_ + EXPECT_CALL(server_mock_, Get(_, testing::StrEq(image_path_), _)) + .WillOnce(SendFast(kBlankPngResponse[0], + std::string(kBlankPngResponse[1], + kBlankPngFileLength))); + + // top.html contains a form. Fill in the username field and submit, causing + // the value to be stored in Chrome's form data DB. + EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) + .WillOnce(testing::DoAll( + AccLeftClickInRenderer(&ie_mock_, AccObjectMatcher(L"username")), + PostCharMessagesToRenderer(&ie_mock_, WideToASCII(kFormFieldValue)), + AccLeftClickInRenderer(&ie_mock_, AccObjectMatcher(L"Submit")))); + + // OnLoad of the result page from form submission. Now close the browser. + EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) + .WillOnce(testing::DoAll( + WatchRendererProcess(&ie_process_exit_watcher_mock_, &ie_mock_), + CloseBrowserMock(&ie_mock_))); + + // Wait until the process is gone, so that the Chrome databases are unlocked. + EXPECT_CALL(ie_mock_, OnQuit()); + + // Verify that the submitted username is in the database, then launch a new + // IE instance. + EXPECT_CALL(ie_process_exit_watcher_mock_, OnObjectSignaled(_)) + .WillOnce(testing::DoAll( + ExpectFormValuesForElementNameMatch( + kFormFieldName, testing::Contains(kFormFieldValue)), + LaunchThisIEAndNavigate(&ie_mock2_, top_url))); + + // Second launch won't load the image due to the cache. + + // We do the delete private data twice, each time toggling the state of the + // 'Delete form data' and 'Delete temporary files' options. + // That's because we have no way to know their initial states. Using this, + // trick we are guaranteed to run it exactly once with each option turned on. + // Running it once with the option turned off is harmless. + + // Proceed to open up the "Safety" menu for the first time through the loop. + EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) + .WillOnce(AccDoDefaultActionInBrowser(&ie_mock2_, + AccObjectMatcher(L"Safety"))); + + for (int i = 0; i < 2; ++i) { + // Watch for the popup menu, click 'Delete Browsing History...' + EXPECT_CALL(acc_observer_, OnMenuPopup(_)).WillOnce( + AccLeftClick(AccObjectMatcher(L"Delete Browsing History...*"))); + + // When it shows up, toggle the options and click "Delete". + EXPECT_CALL(delete_browsing_history_window_observer_mock_, OnWindowOpen(_)) + .WillOnce(testing::DoAll( + AccLeftClick(AccObjectMatcher(L"Temporary Internet files")), + AccLeftClick(AccObjectMatcher(L"Form data")), + AccLeftClick(AccObjectMatcher(L"Delete")))); + + // The configuration dialog closes. + EXPECT_CALL(delete_browsing_history_window_observer_mock_, + OnWindowClose(_)); + + // The progress dialog that pops up has the same caption. + EXPECT_CALL(delete_browsing_history_window_observer_mock_, + OnWindowOpen(_)); + + // Watch for it to go away, then close the browser. + if (i == 0) { + EXPECT_CALL(delete_browsing_history_window_observer_mock_, + OnWindowClose(_)).WillOnce(testing::DoAll( + AccExpectInRenderer(&ie_mock2_, + AccObjectMatcher(L"Blank image.")), + AccDoDefaultActionInBrowser(&ie_mock2_, + AccObjectMatcher(L"Safety")))); + } else { + EXPECT_CALL(delete_browsing_history_window_observer_mock_, + OnWindowClose(_)).WillOnce(testing::DoAll( + AccExpectInRenderer(&ie_mock2_, + AccObjectMatcher(L"Blank image.")), + CloseBrowserMock(&ie_mock2_))); + } + } + + // Close IE, wait for the process to exit... + EXPECT_CALL(ie_mock2_, OnQuit()) + .WillOnce(WatchBrowserProcess(&ie_process_exit_watcher_mock_, + &ie_mock2_)); + + // ... and verify that the remembered form data is not in the form data DB. + EXPECT_CALL(ie_process_exit_watcher_mock_, OnObjectSignaled(_)) + .WillOnce(testing::DoAll( + ExpectFormValuesForElementNameMatch( + kFormFieldName, testing::Not(testing::Contains(kFormFieldValue))), + LaunchThisIEAndNavigate(&ie_mock3_, top_url))); + + // Now that the cache is cleared, final session should load the image from the + // server. + EXPECT_CALL(server_mock_, Get(_, testing::StrEq(image_path_), _)).WillOnce( + SendFast(kBlankPngResponse[0], std::string(kBlankPngResponse[1], + kBlankPngFileLength))); + EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) + .WillOnce(CloseBrowserMock(&ie_mock3_)); + EXPECT_CALL(ie_mock3_, OnQuit()).WillOnce(QUIT_LOOP(loop_)); + + // Start it up. Everything else is triggered as mock actions. + ASSERT_HRESULT_SUCCEEDED( + ie_mock_.event_sink()->LaunchIEAndNavigate(top_url, &ie_mock_)); + + // 3 navigations + 2 invocations of delete browser history == 5 + loop_.RunFor(kChromeFrameLongNavigationTimeoutInSeconds * 5); +} + +} // namespace chrome_frame_test diff --git a/chrome_frame/test/mock_ie_event_sink_actions.h b/chrome_frame/test/mock_ie_event_sink_actions.h index d6d8160..7c9b38b 100644 --- a/chrome_frame/test/mock_ie_event_sink_actions.h +++ b/chrome_frame/test/mock_ie_event_sink_actions.h @@ -6,6 +6,7 @@ #define CHROME_FRAME_TEST_MOCK_IE_EVENT_SINK_ACTIONS_H_ #include <windows.h> +#include <string> #include "base/basictypes.h" #include "base/platform_thread.h" @@ -126,28 +127,35 @@ ACTION_P2(AccSetValue, matcher, value) { } } +namespace { // NOLINT +template<typename R> R AccInWindow(testing::Action<R(HWND)> action, HWND hwnd) { + return action.Perform(typename testing::Action<R(HWND)>::ArgumentTuple(hwnd)); +} +} + +ACTION_P2(AccDoDefaultActionInBrowser, mock, matcher) { + AccInWindow<void>(AccDoDefaultAction(matcher), + mock->event_sink()->GetBrowserWindow()); +} + ACTION_P2(AccDoDefaultActionInRenderer, mock, matcher) { - HWND window = mock->event_sink()->GetRendererWindow(); - scoped_refptr<AccObject> object; - if (FindAccObjectInWindow(window, matcher, &object)) { - EXPECT_TRUE(object->DoDefaultAction()); - } + AccInWindow<void>(AccDoDefaultAction(matcher), + mock->event_sink()->GetRendererWindow()); } ACTION_P2(AccLeftClickInBrowser, mock, matcher) { - HWND window = mock->event_sink()->GetBrowserWindow(); - scoped_refptr<AccObject> object; - if (FindAccObjectInWindow(window, matcher, &object)) { - EXPECT_TRUE(object->LeftClick()); - } + AccInWindow<void>(AccLeftClick(matcher), + mock->event_sink()->GetBrowserWindow()); +} + +ACTION_P2(AccLeftClickInRenderer, mock, matcher) { + AccInWindow<void>(AccLeftClick(matcher), + mock->event_sink()->GetRendererWindow()); } ACTION_P3(AccSetValueInBrowser, mock, matcher, value) { - HWND window = mock->event_sink()->GetBrowserWindow(); - scoped_refptr<AccObject> object; - if (FindAccObjectInWindow(window, matcher, &object)) { - EXPECT_TRUE(object->SetValue(value)); - } + AccInWindow<void>(AccSetValue(matcher, value), + mock->event_sink()->GetBrowserWindow()); } ACTION_P2(AccWatchForOneValueChange, observer, matcher) { @@ -172,8 +180,8 @@ ACTION_P(PostCharMessage, character_code) { } ACTION_P2(PostCharMessageToRenderer, mock, character_code) { - HWND window = mock->event_sink()->GetRendererWindow(); - ::PostMessage(window, WM_CHAR, character_code, 0); + AccInWindow<void>(PostCharMessage(character_code), + mock->event_sink()->GetRendererWindow()); } ACTION_P2(PostCharMessagesToRenderer, mock, character_codes) { @@ -191,7 +199,21 @@ ACTION_P(StopWindowWatching, mock) { mock->StopWatching(); } -namespace { +ACTION_P(WatchWindowProcess, mock_observer) { + mock_observer->WatchProcessForHwnd(arg0); +} + +ACTION_P2(WatchBrowserProcess, mock_observer, mock) { + AccInWindow<void>(WatchWindowProcess(mock_observer), + mock->event_sink()->GetBrowserWindow()); +} + +ACTION_P2(WatchRendererProcess, mock_observer, mock) { + AccInWindow<void>(WatchWindowProcess(mock_observer), + mock->event_sink()->GetRendererWindow()); +} + +namespace { // NOLINT void DoCloseWindowNow(HWND hwnd) { ::PostMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0); @@ -218,6 +240,15 @@ ACTION(KillChromeFrameProcesses) { } // Verifying actions +ACTION_P(AccExpect, matcher) { + scoped_refptr<AccObject> object; + EXPECT_TRUE(FindAccObjectInWindow(arg0, matcher, &object)); +} + +ACTION_P2(AccExpectInRenderer, mock, matcher) { + AccInWindow<void>(AccExpect(matcher), + mock->event_sink()->GetRendererWindow()); +} ACTION_P(ExpectRendererHasFocus, mock) { mock->event_sink()->ExpectRendererWindowHasFocus(); diff --git a/chrome_frame/test/mock_ie_event_sink_test.h b/chrome_frame/test/mock_ie_event_sink_test.h index 4a1e31d..74be974 100644 --- a/chrome_frame/test/mock_ie_event_sink_test.h +++ b/chrome_frame/test/mock_ie_event_sink_test.h @@ -8,6 +8,7 @@ #include <atlbase.h> #include <atlcom.h> #include <string> +#include <vector> #include "base/file_path.h" #include "base/string_number_conversions.h" @@ -203,18 +204,55 @@ class MockPropertyNotifySinkListener : public PropertyNotifySinkListener { ScopedComPtr<IUnknown> event_source_; }; +// Allows tests to observe when processes exit. +class MockObjectWatcherDelegate : public base::ObjectWatcher::Delegate { + public: + // base::ObjectWatcher::Delegate implementation + MOCK_METHOD1(OnObjectSignaled, void (HANDLE process_handle)); // NOLINT + + virtual ~MockObjectWatcherDelegate() { + // Would be nice to free them when OnObjectSignaled is called, too, but + // it doesn't seem worth it. + for (std::vector<HANDLE>::iterator it = process_handles_.begin(); + it != process_handles_.end(); ++it) { + ::CloseHandle(*it); + } + } + + // Registers this instance to watch |process_handle| for termination. + void WatchProcess(HANDLE process_handle) { + process_handles_.push_back(process_handle); + object_watcher_.StartWatching(process_handle, this); + } + + // Registers this instance to watch |hwnd|'s owning process for termination. + void WatchProcessForHwnd(HWND hwnd) { + DWORD pid = 0; + ::GetWindowThreadProcessId(hwnd, &pid); + EXPECT_TRUE(pid); + if (pid != 0) { + HANDLE process_handle = ::OpenProcess(SYNCHRONIZE, FALSE, pid); + EXPECT_TRUE(process_handle); + if (process_handle != NULL) { + WatchProcess(process_handle); + } + } + } + + private: + std::vector<HANDLE> process_handles_; + base::ObjectWatcher object_watcher_; +}; // Mocks a window observer so that tests can detect new windows. class MockWindowObserver : public WindowObserver { public: - // Override WindowObserver methods. - MOCK_METHOD2(OnWindowDetected, void (HWND hwnd, // NOLINT - const std::string& caption)); - - // Watch for all windows of the given class type. - void WatchWindow(const wchar_t* window_class) { - DCHECK(window_class); - window_watcher_.AddObserver(this, WideToUTF8(window_class)); + // WindowObserver implementation + MOCK_METHOD1(OnWindowOpen, void (HWND hwnd)); // NOLINT + MOCK_METHOD1(OnWindowClose, void (HWND hwnd)); // NOLINT + + void WatchWindow(std::string caption_pattern) { + window_watcher_.AddObserver(this, caption_pattern); } private: diff --git a/chrome_frame/test/navigation_test.cc b/chrome_frame/test/navigation_test.cc index 0701a30..62cb876 100644 --- a/chrome_frame/test/navigation_test.cc +++ b/chrome_frame/test/navigation_test.cc @@ -287,6 +287,7 @@ TEST_P(FullTabNavigationTest, FLAKY_RestrictedSite) { return; } MockWindowObserver win_observer_mock; + ScopedComPtr<IInternetSecurityManager> security_manager; HRESULT hr = security_manager.CreateInstance(CLSID_InternetSecurityManager); ASSERT_HRESULT_SUCCEEDED(hr); @@ -294,25 +295,25 @@ TEST_P(FullTabNavigationTest, FLAKY_RestrictedSite) { hr = security_manager->SetZoneMapping(URLZONE_UNTRUSTED, GetTestUrl(L"").c_str(), SZM_CREATE); - EXPECT_CALL(ie_mock_, OnFileDownload(_, _)) - .Times(testing::AnyNumber()); + EXPECT_CALL(ie_mock_, OnFileDownload(_, _)).Times(testing::AnyNumber()); server_mock_.ExpectAndServeAnyRequests(GetParam()); ProtocolPatchMethod patch_method = GetPatchMethod(); - const wchar_t* kDialogClass = L"#32770"; const char* kAlertDlgCaption = "Security Alert"; - EXPECT_CALL(ie_mock_, OnBeforeNavigate2(_, - testing::Field(&VARIANT::bstrVal, - testing::StrCaseEq(GetSimplePageUrl())), _, _, _, _, _)) - .Times(1) - .WillOnce(WatchWindow(&win_observer_mock, kDialogClass)); + EXPECT_CALL(ie_mock_, OnBeforeNavigate2( + _, + testing::Field(&VARIANT::bstrVal, testing::StrCaseEq(GetSimplePageUrl())), + _, _, _, _, _)) + .Times(1) + .WillOnce(WatchWindow(&win_observer_mock, kAlertDlgCaption)); if (patch_method == PATCH_METHOD_INET_PROTOCOL) { - EXPECT_CALL(ie_mock_, OnBeforeNavigate2(_, - testing::Field(&VARIANT::bstrVal, - testing::HasSubstr(L"res://")), _, _, _, _, _)) + EXPECT_CALL(ie_mock_, OnBeforeNavigate2( + _, + testing::Field(&VARIANT::bstrVal, testing::HasSubstr(L"res://")), + _, _, _, _, _)) .Times(testing::AtMost(1)); } @@ -320,11 +321,12 @@ TEST_P(FullTabNavigationTest, FLAKY_RestrictedSite) { testing::Field(&VARIANT::bstrVal, StrEq(GetSimplePageUrl())))) .Times(testing::AtMost(1)); - EXPECT_CALL(win_observer_mock, OnWindowDetected(_, StrEq(kAlertDlgCaption))) + EXPECT_CALL(win_observer_mock, OnWindowOpen(_)) .Times(1) - .WillOnce(testing::DoAll( - DoCloseWindow(), - CloseBrowserMock(&ie_mock_))); + .WillOnce(DoCloseWindow()); + EXPECT_CALL(win_observer_mock, OnWindowClose(_)) + .Times(1) + .WillOnce(CloseBrowserMock(&ie_mock_)); LaunchIEAndNavigate(GetSimplePageUrl()); diff --git a/chrome_frame/test/net/fake_external_tab.cc b/chrome_frame/test/net/fake_external_tab.cc index b42b13b..59d4f84 100644 --- a/chrome_frame/test/net/fake_external_tab.cc +++ b/chrome_frame/test/net/fake_external_tab.cc @@ -105,7 +105,8 @@ class SupplyProxyCredentials : public WindowObserver { HWND password_; }; - virtual void OnWindowDetected(HWND hwnd, const std::string& caption); + virtual void OnWindowOpen(HWND hwnd); + virtual void OnWindowClose(HWND hwnd); static BOOL CALLBACK EnumChildren(HWND hwnd, LPARAM param); protected: @@ -119,12 +120,9 @@ SupplyProxyCredentials::SupplyProxyCredentials(const char* username, : username_(username), password_(password) { } -void SupplyProxyCredentials::OnWindowDetected(HWND hwnd, - const std::string& caption) { - // IE's dialog caption (en-US). - if (caption.compare("Windows Security") != 0) - return; +void SupplyProxyCredentials::OnWindowClose(HWND hwnd) { } +void SupplyProxyCredentials::OnWindowOpen(HWND hwnd) { DialogProps props = {0}; ::EnumChildWindows(hwnd, EnumChildren, reinterpret_cast<LPARAM>(&props)); DCHECK(::IsWindow(props.username_)); @@ -474,8 +472,7 @@ int main(int argc, char** argv) { WindowWatchdog watchdog; // See url_request_unittest.cc for these credentials. SupplyProxyCredentials credentials("user", "secret"); - // Check for a dialog class ("#32770") - watchdog.AddObserver(&credentials, "#32770"); + watchdog.AddObserver(&credentials, "Windows Security"); testing::InitGoogleTest(&argc, argv); FilterDisabledTests(); PluginService::EnableChromePlugins(false); diff --git a/chrome_frame/test/ui_test.cc b/chrome_frame/test/ui_test.cc index 54fd0d2..6224d80 100644 --- a/chrome_frame/test/ui_test.cc +++ b/chrome_frame/test/ui_test.cc @@ -112,23 +112,23 @@ TEST_P(FullTabUITest, FLAKY_CtrlN) { // events for New Window, but for Crl+N we don't get any // OnNewWindowX notifications. :( MockWindowObserver win_observer_mock; - const wchar_t* kIEFrameClass = L"IEFrame"; + + const char* kNewWindowTitlePattern = "*Internet Explorer*"; EXPECT_CALL(ie_mock_, OnLoad(is_cf, StrEq(GetSimplePageUrl()))) .WillOnce(testing::DoAll( - WatchWindow(&win_observer_mock, kIEFrameClass), + WatchWindow(&win_observer_mock, kNewWindowTitlePattern), SetFocusToRenderer(&ie_mock_), DelaySendChar(&loop_, 1000, 'n', simulate_input::CONTROL))); - // Watch for new window - const char* kNewWindowTitle = "Internet Explorer"; - EXPECT_CALL(win_observer_mock, - OnWindowDetected(_, testing::HasSubstr(kNewWindowTitle))) - .WillOnce(testing::DoAll( - DoCloseWindow(), - CloseBrowserMock(&ie_mock_))); + // Watch for new window. It appears that the window close message cannot be + // reliably delivered immediately upon receipt of the window open event. + EXPECT_CALL(win_observer_mock, OnWindowOpen(_)) + .WillOnce(DelayDoCloseWindow(500)); + + EXPECT_CALL(win_observer_mock, OnWindowClose(_)) + .WillOnce(CloseBrowserMock(&ie_mock_)); LaunchIEAndNavigate(GetSimplePageUrl()); - // TODO(kkania): The new window does not close properly sometimes. } // Test that ctrl+r does cause a refresh. @@ -389,20 +389,20 @@ TEST_F(ContextMenuTest, CFPageInfo) { InSequence expect_in_sequence_for_scope; // View page information. - const wchar_t* kPageInfoWindowClass = L"Chrome_WidgetWin_0"; + const char* kPageInfoCaption = "Security Information"; EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) .WillOnce(testing::DoAll( - WatchWindow(&win_observer_mock, kPageInfoWindowClass), + WatchWindow(&win_observer_mock, kPageInfoCaption), OpenContextMenuAsync())); EXPECT_CALL(acc_observer_, OnMenuPopup(_)) .WillOnce(AccLeftClick(AccObjectMatcher(L"View page info"))); // Expect page info dialog to pop up. Dismiss the dialog with 'Esc' key - const char* kPageInfoCaption = "Security Information"; - EXPECT_CALL(win_observer_mock, OnWindowDetected(_, StrEq(kPageInfoCaption))) - .WillOnce(testing::DoAll( - DoCloseWindow(), - CloseBrowserMock(&ie_mock_))); + EXPECT_CALL(win_observer_mock, OnWindowOpen(_)) + .WillOnce(DoCloseWindow()); + + EXPECT_CALL(win_observer_mock, OnWindowClose(_)) + .WillOnce(CloseBrowserMock(&ie_mock_)); LaunchIEAndNavigate(GetSimplePageUrl()); } @@ -413,22 +413,20 @@ TEST_F(ContextMenuTest, CFInspector) { InSequence expect_in_sequence_for_scope; // Open developer tools. - const wchar_t* kPageInfoWindowClass = L"Chrome_WidgetWin_0"; + // Devtools begins life with "Untitled" caption and it changes + // later to the 'Developer Tools - <url> form. + const char* kPageInfoCaptionPattern = "Untitled*"; EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) .WillOnce(testing::DoAll( - WatchWindow(&win_observer_mock, kPageInfoWindowClass), + WatchWindow(&win_observer_mock, kPageInfoCaptionPattern), OpenContextMenuAsync())); EXPECT_CALL(acc_observer_, OnMenuPopup(_)) .WillOnce(AccLeftClick(AccObjectMatcher(L"Inspect element"))); - // Devtools begins life with "Untitled" caption and it changes - // later to the 'Developer Tools - <url> form. - const char* kPageInfoCaption = "Untitled"; - EXPECT_CALL(win_observer_mock, - OnWindowDetected(_, testing::StartsWith(kPageInfoCaption))) - .WillOnce(testing::DoAll( - DelayDoCloseWindow(5000), // wait to catch possible crash - DelayCloseBrowserMock(&loop_, 5500, &ie_mock_))); + EXPECT_CALL(win_observer_mock, OnWindowOpen(_)) + .WillOnce(DelayDoCloseWindow(5000)); // wait to catch possible crash + EXPECT_CALL(win_observer_mock, OnWindowClose(_)) + .WillOnce(CloseBrowserMock(&ie_mock_)); LaunchIENavigateAndLoop(GetSimplePageUrl(), kChromeFrameLongNavigationTimeoutInSeconds * 2); @@ -437,13 +435,14 @@ TEST_F(ContextMenuTest, CFInspector) { TEST_F(ContextMenuTest, CFSaveAs) { server_mock_.ExpectAndServeAnyRequests(CFInvocation::MetaTag()); MockWindowObserver win_observer_mock; + InSequence expect_in_sequence_for_scope; // Open 'Save As' dialog. - const wchar_t* kSaveDlgClass = L"#32770"; + const char* kSaveDlgCaption = "Save As"; EXPECT_CALL(acc_observer_, OnAccDocLoad(_)) .WillOnce(testing::DoAll( - WatchWindow(&win_observer_mock, kSaveDlgClass), + WatchWindow(&win_observer_mock, kSaveDlgCaption), OpenContextMenuAsync())); EXPECT_CALL(acc_observer_, OnMenuPopup(_)) .WillOnce(AccLeftClick(AccObjectMatcher(L"Save as..."))); @@ -454,12 +453,14 @@ TEST_F(ContextMenuTest, CFSaveAs) { temp_file_path = temp_file_path.ReplaceExtension(L".htm"); ASSERT_TRUE(file_util::DieFileDie(temp_file_path, false)); - EXPECT_CALL(win_observer_mock, OnWindowDetected(_, StrEq("Save As"))) + EXPECT_CALL(win_observer_mock, OnWindowOpen(_)) .WillOnce(testing::DoAll( AccSetValue(AccObjectMatcher(L"File name:", L"", L"simple*"), - temp_file_path.value()), - AccDoDefaultAction(AccObjectMatcher(L"Save", L"push button")), - CloseWhenFileSaved(&ie_mock_, temp_file_path, 5000))); + temp_file_path.value()), + AccDoDefaultAction(AccObjectMatcher(L"Save", L"push button")))); + + EXPECT_CALL(win_observer_mock, OnWindowClose(_)) + .WillOnce(CloseWhenFileSaved(&ie_mock_, temp_file_path, 5000)); LaunchIENavigateAndLoop(GetSimplePageUrl(), kChromeFrameLongNavigationTimeoutInSeconds * 2); diff --git a/chrome_frame/test/win_event_receiver.cc b/chrome_frame/test/win_event_receiver.cc index e6295cd..c3ae0b3 100644 --- a/chrome_frame/test/win_event_receiver.cc +++ b/chrome_frame/test/win_event_receiver.cc @@ -5,6 +5,8 @@ #include "chrome_frame/test/win_event_receiver.h" #include "base/logging.h" +#include "base/message_loop.h" +#include "base/object_watcher.h" #include "base/string_util.h" #include "chrome_frame/function_stub.h" @@ -68,18 +70,93 @@ void WinEventReceiver::WinEventHook(WinEventReceiver* me, HWINEVENTHOOK hook, me->listener_->OnEventReceived(event, hwnd, object_id, child_id); } -// WindowWatchdog methods +// Notifies WindowWatchdog when the process owning a given window exits. +// +// If the process terminates before its handle may be obtained, this class will +// still properly notifyy the WindowWatchdog. +// +// Notification is always delivered via a message loop task in the message loop +// that is active when the instance is constructed. +class WindowWatchdog::ProcessExitObserver + : public base::ObjectWatcher::Delegate { + public: + // Initiates the process watch. Will always return without notifying the + // watchdog. + ProcessExitObserver(WindowWatchdog* window_watchdog, HWND hwnd); + virtual ~ProcessExitObserver(); + + // base::ObjectWatcher::Delegate implementation + virtual void OnObjectSignaled(HANDLE process_handle); + + private: + WindowWatchdog* window_watchdog_; + HANDLE process_handle_; + HWND hwnd_; + + ScopedRunnableMethodFactory<ProcessExitObserver> method_task_factory_; + base::ObjectWatcher object_watcher_; + + DISALLOW_COPY_AND_ASSIGN(ProcessExitObserver); +}; + +WindowWatchdog::ProcessExitObserver::ProcessExitObserver( + WindowWatchdog* window_watchdog, HWND hwnd) + : window_watchdog_(window_watchdog), + process_handle_(NULL), + hwnd_(hwnd), + ALLOW_THIS_IN_INITIALIZER_LIST(method_task_factory_(this)) { + DWORD pid = 0; + ::GetWindowThreadProcessId(hwnd, &pid); + if (pid != 0) { + process_handle_ = ::OpenProcess(SYNCHRONIZE, FALSE, pid); + } + + if (process_handle_ != NULL) { + object_watcher_.StartWatching(process_handle_, this); + } else { + // Process is gone, so the window must be gone too. Notify our observer! + MessageLoop::current()->PostTask( + FROM_HERE, + method_task_factory_.NewRunnableMethod( + &ProcessExitObserver::OnObjectSignaled, HANDLE(NULL))); + } +} + +WindowWatchdog::ProcessExitObserver::~ProcessExitObserver() { + if (process_handle_ != NULL) { + ::CloseHandle(process_handle_); + } +} + +void WindowWatchdog::ProcessExitObserver::OnObjectSignaled( + HANDLE process_handle) { + window_watchdog_->OnHwndProcessExited(hwnd_); +} + +WindowWatchdog::WindowWatchdog() {} + void WindowWatchdog::AddObserver(WindowObserver* observer, - const std::string& window_class) { - if (observers_.empty()) - win_event_receiver_.SetListenerForEvent(this, EVENT_OBJECT_SHOW); + const std::string& caption_pattern) { + if (observers_.empty()) { + // SetListenerForEvents takes an event_min and event_max. + // EVENT_OBJECT_DESTROY, EVENT_OBJECT_SHOW, and EVENT_OBJECT_HIDE are + // consecutive, in that order; hence we supply only DESTROY and HIDE to + // denote exactly the required set. + win_event_receiver_.SetListenerForEvents( + this, EVENT_OBJECT_DESTROY, EVENT_OBJECT_HIDE); + } + + ObserverEntry new_entry = { + observer, + caption_pattern, + OpenWindowList() }; - WindowObserverEntry new_entry = { observer, window_class }; observers_.push_back(new_entry); } void WindowWatchdog::RemoveObserver(WindowObserver* observer) { - for (ObserverMap::iterator i = observers_.begin(); i != observers_.end();) { + for (ObserverEntryList::iterator i = observers_.begin(); + i != observers_.end(); ) { i = (observer == i->observer) ? observers_.erase(i) : ++i; } @@ -87,31 +164,93 @@ void WindowWatchdog::RemoveObserver(WindowObserver* observer) { win_event_receiver_.StopReceivingEvents(); } -void WindowWatchdog::OnEventReceived(DWORD event, HWND hwnd, LONG object_id, - LONG child_id) { - // We need to look for top level windows and a natural check is for - // WS_CHILD. Instead, checking for WS_CAPTION allows us to filter - // out other stray popups - if (!(WS_CAPTION & GetWindowLong(hwnd, GWL_STYLE))) - return; +std::string WindowWatchdog::GetWindowCaption(HWND hwnd) { + std::string caption; + int len = ::GetWindowTextLength(hwnd) + 1; + ::GetWindowTextA(hwnd, WriteInto(&caption, len), len); - char class_name[MAX_PATH] = {0}; - ::GetClassNameA(hwnd, class_name, arraysize(class_name)); + return caption; +} + +void WindowWatchdog::HandleOnOpen(HWND hwnd) { + std::string caption = GetWindowCaption(hwnd); + + // Instantiated only if there is at least one interested observer. Each + // interested observer will maintain a reference to this object, such that it + // is deleted when the last observer disappears. + linked_ptr<ProcessExitObserver> process_exit_observer; + + // Identify the interested observers and mark them as watching this HWND for + // close. + ObserverEntryList interested_observers; + for (ObserverEntryList::iterator entry_iter = observers_.begin(); + entry_iter != observers_.end(); ++entry_iter) { + if (MatchPattern(caption, entry_iter->caption_pattern)) { + if (process_exit_observer == NULL) { + process_exit_observer.reset(new ProcessExitObserver(this, hwnd)); + } - ObserverMap interested_observers; - for (ObserverMap::iterator i = observers_.begin(); - i != observers_.end(); i++) { - if (0 == lstrcmpA(i->window_class.c_str(), class_name)) { - interested_observers.push_back(*i); + entry_iter->open_windows.push_back( + OpenWindowEntry(hwnd, process_exit_observer)); + + interested_observers.push_back(*entry_iter); } } - std::string caption; - int len = ::GetWindowTextLength(hwnd) + 1; - ::GetWindowTextA(hwnd, WriteInto(&caption, len), len); + // Notify the interested observers in a separate pass in case AddObserver or + // RemoveObserver is called as a side-effect of the notification. + for (ObserverEntryList::iterator entry_iter = interested_observers.begin(); + entry_iter != interested_observers.end(); ++entry_iter) { + entry_iter->observer->OnWindowOpen(hwnd); + } +} + +void WindowWatchdog::HandleOnClose(HWND hwnd) { + // Identify the interested observers, reaping OpenWindow entries as + // appropriate + ObserverEntryList interested_observers; + for (ObserverEntryList::iterator entry_iter = observers_.begin(); + entry_iter != observers_.end(); ++entry_iter) { + size_t num_open_windows = entry_iter->open_windows.size(); + + OpenWindowList::iterator window_iter = entry_iter->open_windows.begin(); + while (window_iter != entry_iter->open_windows.end()) { + if (hwnd == window_iter->first) { + window_iter = entry_iter->open_windows.erase(window_iter); + } else { + ++window_iter; + } + } - for (ObserverMap::iterator i = interested_observers.begin(); - i != interested_observers.end(); i++) { - i->observer->OnWindowDetected(hwnd, caption); + if (num_open_windows != entry_iter->open_windows.size()) { + interested_observers.push_back(*entry_iter); + } + } + + // Notify the interested observers in a separate pass in case AddObserver or + // RemoveObserver is called as a side-effect of the notification. + for (ObserverEntryList::iterator entry_iter = interested_observers.begin(); + entry_iter != interested_observers.end(); ++entry_iter) { + entry_iter->observer->OnWindowClose(hwnd); } } + +void WindowWatchdog::OnEventReceived( + DWORD event, HWND hwnd, LONG object_id, LONG child_id) { + // We need to look for top level windows and a natural check is for + // WS_CHILD. Instead, checking for WS_CAPTION allows us to filter + // out other stray popups + if (!WS_CAPTION & GetWindowLong(hwnd, GWL_STYLE)) { + return; + } + if (event == EVENT_OBJECT_SHOW) { + HandleOnOpen(hwnd); + } else { + DCHECK(event == EVENT_OBJECT_DESTROY || event == EVENT_OBJECT_HIDE); + HandleOnClose(hwnd); + } +} + +void WindowWatchdog::OnHwndProcessExited(HWND hwnd) { + HandleOnClose(hwnd); +} diff --git a/chrome_frame/test/win_event_receiver.h b/chrome_frame/test/win_event_receiver.h index c330520..d91f268 100644 --- a/chrome_frame/test/win_event_receiver.h +++ b/chrome_frame/test/win_event_receiver.h @@ -9,6 +9,10 @@ #include <string> #include <vector> +#include <utility> + +#include "base/linked_ptr.h" +#include "base/object_watcher.h" struct FunctionStub; @@ -54,39 +58,79 @@ class WinEventReceiver { FunctionStub* hook_stub_; }; -// Observes window show events. Used with WindowWatchdog. +// Receives notifications when a window is opened or closed. class WindowObserver { public: virtual ~WindowObserver() {} - // Called when a window has been shown. - virtual void OnWindowDetected(HWND hwnd, const std::string& caption) = 0; + virtual void OnWindowOpen(HWND hwnd) = 0; + virtual void OnWindowClose(HWND hwnd) = 0; }; -// Watch a for window to be shown with the given window class name. -// If found, call the observer interested in it. +// Notifies observers when windows whose captions match specified patterns +// open or close. When a window opens, its caption is compared to the patterns +// associated with each observer. Observers registered with matching patterns +// are notified of the window's opening and will be notified when the same +// window is closed (including if the owning process terminates without closing +// the window). +// +// Changes to a window's caption while it is open do not affect the set of +// observers to be notified when it closes. +// +// Observers are not notified of the closing of windows that were already open +// when they were registered. +// +// Observers may call AddObserver and/or RemoveObserver during notifications. +// +// Each instance of this class must only be accessed from a single thread, and +// that thread must be running a message loop. class WindowWatchdog : public WinEventListener { public: - // Register for notifications for |window_class|. An observer can register - // for multiple notifications. - void AddObserver(WindowObserver* observer, const std::string& window_class); - - // Remove all entries for |observer|. + WindowWatchdog(); + // Register |observer| to be notified when windows matching |caption_pattern| + // are opened or closed. A single observer may be registered multiple times. + // If a single window caption matches multiple registrations of a single + // observer, the observer will be notified once per matching registration. + void AddObserver(WindowObserver* observer, + const std::string& caption_pattern); + + // Remove all registrations of |observer|. The |observer| will not be notified + // during or after this call. void RemoveObserver(WindowObserver* observer); private: - struct WindowObserverEntry { + class ProcessExitObserver; + + // The Delegate object is actually a ProcessExitObserver, but declaring + // it as such would require fully declaring the ProcessExitObserver class + // here in order for linked_ptr to access its destructor. + typedef std::pair<HWND, linked_ptr<base::ObjectWatcher::Delegate> > + OpenWindowEntry; + typedef std::vector<OpenWindowEntry> OpenWindowList; + + struct ObserverEntry { WindowObserver* observer; - std::string window_class; + std::string caption_pattern; + OpenWindowList open_windows; }; - typedef std::vector<WindowObserverEntry> ObserverMap; + typedef std::vector<ObserverEntry> ObserverEntryList; - // Overriden from WinEventListener. - virtual void OnEventReceived(DWORD event, HWND hwnd, LONG object_id, - LONG child_id); + // WinEventListener implementation. + virtual void OnEventReceived( + DWORD event, HWND hwnd, LONG object_id, LONG child_id); - ObserverMap observers_; + static std::string GetWindowCaption(HWND hwnd); + + void HandleOnOpen(HWND hwnd); + void HandleOnClose(HWND hwnd); + void OnHwndProcessExited(HWND hwnd); + + ObserverEntryList observers_; WinEventReceiver win_event_receiver_; + + DISALLOW_COPY_AND_ASSIGN(WindowWatchdog); }; + + #endif // CHROME_FRAME_TEST_WIN_EVENT_RECEIVER_H_ |