// 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 #import #import #include #include "webkit/tools/test_shell/test_shell.h" #include "base/base_paths.h" #include "base/basictypes.h" #include "base/debug/debugger.h" #include "base/file_path.h" #include "base/file_util.h" #include "base/logging.h" #include "base/mac/mac_util.h" #include "base/memory/memory_debug.h" #include "base/message_loop.h" #include "base/path_service.h" #include "base/string16.h" #include "base/string_piece.h" #include "base/utf_string_conversions.h" #include "grit/webkit_resources.h" #include "net/base/mime_util.h" #include "skia/ext/bitmap_platform_device.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" #include "ui/base/resource/data_pack.h" #include "ui/gfx/size.h" #include "webkit/glue/webkit_glue.h" #include "webkit/glue/webpreferences.h" #include "webkit/plugins/npapi/plugin_list.h" #include "webkit/tools/test_shell/mac/test_shell_webview.h" #include "webkit/tools/test_shell/resource.h" #include "webkit/tools/test_shell/simple_resource_loader_bridge.h" #include "webkit/tools/test_shell/test_navigation_controller.h" #include "webkit/tools/test_shell/test_webview_delegate.h" #include "third_party/skia/include/core/SkBitmap.h" #import "mac/DumpRenderTreePasteboard.h" using WebKit::WebWidget; #define MAX_LOADSTRING 100 // Sizes for URL bar layout #define BUTTON_HEIGHT 22 #define BUTTON_WIDTH 72 #define BUTTON_MARGIN 8 #define URLBAR_HEIGHT 32 // Global Variables: // Content area size for newly created windows. const int kTestWindowWidth = 800; const int kTestWindowHeight = 600; // The W3C SVG layout tests use a different size than the other layout tests const int kSVGTestWindowWidth = 480; const int kSVGTestWindowHeight = 360; // Hide the window offscreen when in layout test mode. Mac OS X limits // window positions to +/- 16000. const int kTestWindowXLocation = -14000; const int kTestWindowYLocation = -14000; // Data pack resource. This is a pointer to the mmapped resources file. static ui::DataPack* g_resource_data_pack = NULL; // Define static member variables base::LazyInstance > TestShell::window_map_(base::LINKER_INITIALIZED); // Helper method for getting the path to the test shell resources directory. FilePath GetResourcesFilePath() { FilePath path; // We need to know if we're bundled or not to know which path to use. if (base::mac::AmIBundled()) { PathService::Get(base::DIR_EXE, &path); path = path.Append(FilePath::kParentDirectory); return path.AppendASCII("Resources"); } else { PathService::Get(base::DIR_SOURCE_ROOT, &path); path = path.AppendASCII("webkit"); path = path.AppendASCII("tools"); path = path.AppendASCII("test_shell"); return path.AppendASCII("resources"); } } // Receives notification that the window is closing so that it can start the // tear-down process. Is responsible for deleting itself when done. @interface WindowDelegate : NSObject { @private TestShellWebView* m_webView; } - (id)initWithWebView:(TestShellWebView*)view; @end @implementation WindowDelegate - (id)initWithWebView:(TestShellWebView*)view { if ((self = [super init])) { m_webView = view; } return self; } - (void)windowDidBecomeKey:(NSNotification*)notification { [m_webView setIsActive:YES]; } - (void)windowDidResignKey:(NSNotification*)notification { [m_webView setIsActive:NO]; } // Called when the window is about to close. Perform the self-destruction // sequence by getting rid of the shell and removing it and the window from // the various global lists. Instead of doing it here, however, we fire off // a delayed call to |-cleanup:| to allow everything to get off the stack // before we go deleting objects. By returning YES, we allow the window to be // removed from the screen. - (BOOL)windowShouldClose:(id)window { m_webView = nil; // Try to make the window go away, but it may not when running layout // tests due to the quirkyness of autorelease pools and having no main loop. [window autorelease]; // clean ourselves up and do the work after clearing the stack of anything // that might have the shell on it. [self performSelectorOnMainThread:@selector(cleanup:) withObject:window waitUntilDone:NO]; return YES; } // does the work of removing the window from our various bookkeeping lists // and gets rid of the shell. - (void)cleanup:(id)window { TestShell::RemoveWindowFromList(window); TestShell::DestroyAssociatedShell(window); [self release]; } @end // Mac-specific stuff to do when the dtor is called. Nothing to do in our // case. void TestShell::PlatformCleanUp() { } void TestShell::EnableUIControl(UIControl control, bool is_enabled) { // TODO(darin): Implement me. } // static void TestShell::DestroyAssociatedShell(gfx::NativeWindow handle) { WindowMap::iterator it = window_map_.Get().find(handle); if (it != window_map_.Get().end()) { // Break the view's association with its shell before deleting the shell. TestShellWebView* web_view = static_cast(it->second->m_webViewHost->view_handle()); if ([web_view isKindOfClass:[TestShellWebView class]]) { [web_view setShell:NULL]; } delete it->second; window_map_.Get().erase(it); } else { LOG(ERROR) << "Failed to find shell for window during destroy"; } } // static void TestShell::PlatformShutdown() { // for each window in the window list, release it and destroy its shell for (WindowList::iterator it = TestShell::windowList()->begin(); it != TestShell::windowList()->end(); ++it) { DestroyAssociatedShell(*it); [*it release]; } // assert if we have anything left over, that would be bad. DCHECK(window_map_.Get().empty()); // Dump the pasteboards we built up. [DumpRenderTreePasteboard releaseLocalPasteboards]; } // static void TestShell::InitializeTestShell(bool layout_test_mode, bool allow_external_pages) { // This should move to a per-process platform-specific initialization function // when one exists. window_list_ = new WindowList; layout_test_mode_ = layout_test_mode; allow_external_pages_ = allow_external_pages; web_prefs_ = new WebPreferences; // mmap the data pack which holds strings used by WebCore. This is only // a fatal error if we're bundled, which means we might be running layout // tests. This is a harmless failure for test_shell_tests. g_resource_data_pack = new ui::DataPack; NSString *resource_path = [base::mac::MainAppBundle() pathForResource:@"test_shell" ofType:@"pak"]; FilePath resources_pak_path([resource_path fileSystemRepresentation]); if (!g_resource_data_pack->Load(resources_pak_path)) { LOG(FATAL) << "failed to load test_shell.pak"; } ResetWebPreferences(); // Load the Ahem font, which is used by layout tests. const char* ahem_path_c; NSString* ahem_path = [[base::mac::MainAppBundle() resourcePath] stringByAppendingPathComponent:@"AHEM____.TTF"]; ahem_path_c = [ahem_path fileSystemRepresentation]; FSRef ahem_fsref; if (!base::mac::FSRefFromPath(ahem_path_c, &ahem_fsref)) { DLOG(FATAL) << "FSRefFromPath " << ahem_path_c; } else { // The last argument is an ATSFontContainerRef that can be passed to // ATSFontDeactivate to unload the font. Since the font is only loaded // for this process, and it's always wanted, don't keep track of it. if (ATSFontActivateFromFileReference(&ahem_fsref, kATSFontContextLocal, kATSFontFormatUnspecified, NULL, kATSOptionFlagsDefault, NULL) != noErr) { DLOG(FATAL) << "ATSFontActivateFromFileReference " << ahem_path_c; } } // Add /plugins to the plugin path so we can load // test plugins. FilePath plugins_dir; PathService::Get(base::DIR_EXE, &plugins_dir); if (base::mac::AmIBundled()) { plugins_dir = plugins_dir.AppendASCII("../../../plugins"); } else { plugins_dir = plugins_dir.AppendASCII("plugins"); } webkit::npapi::PluginList::Singleton()->AddExtraPluginDir(plugins_dir); } NSButton* MakeTestButton(NSRect* rect, NSString* title, NSView* parent) { NSButton* button = [[[NSButton alloc] initWithFrame:*rect] autorelease]; [button setTitle:title]; [button setBezelStyle:NSSmallSquareBezelStyle]; [button setAutoresizingMask:(NSViewMaxXMargin | NSViewMinYMargin)]; [parent addSubview:button]; rect->origin.x += BUTTON_WIDTH; return button; } bool TestShell::Initialize(const GURL& starting_url) { // Perform application initialization: // send message to app controller? need to work this out // TODO(awalker): this is a straight recreation of windows test_shell.cc's // window creation code--we should really pull this from the nib and grab // references to the already-created subviews that way. NSRect screen_rect = [[NSScreen mainScreen] visibleFrame]; NSRect window_rect = { {0, screen_rect.size.height - kTestWindowHeight}, {kTestWindowWidth, kTestWindowHeight} }; m_mainWnd = [[NSWindow alloc] initWithContentRect:window_rect styleMask:(NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask ) backing:NSBackingStoreBuffered defer:NO]; [m_mainWnd setTitle:@"TestShell"]; // Add to our map window_map_.Get()[m_mainWnd] = this; // Rely on the window delegate to clean us up rather than immediately // releasing when the window gets closed. We use the delegate to do // everything from the autorelease pool so the shell isn't on the stack // during cleanup (ie, a window close from javascript). [m_mainWnd setReleasedWhenClosed:NO]; // Create a webview. Note that |web_view| takes ownership of this shell so we // will get cleaned up when it gets destroyed. m_webViewHost.reset( WebViewHost::Create([m_mainWnd contentView], delegate_.get(), 0, *TestShell::web_prefs_)); delegate_->RegisterDragDrop(); TestShellWebView* web_view = static_cast(m_webViewHost->view_handle()); [web_view setShell:this]; // Create a window delegate to watch for when it's asked to go away. It will // clean itself up so we don't need to hold a reference. [m_mainWnd setDelegate:[[WindowDelegate alloc] initWithWebView:web_view]]; // create buttons NSRect button_rect = [[m_mainWnd contentView] bounds]; button_rect.origin.y = window_rect.size.height - URLBAR_HEIGHT + (URLBAR_HEIGHT - BUTTON_HEIGHT) / 2; button_rect.size.height = BUTTON_HEIGHT; button_rect.origin.x += BUTTON_MARGIN; button_rect.size.width = BUTTON_WIDTH; NSView* content = [m_mainWnd contentView]; NSButton* button = MakeTestButton(&button_rect, @"Back", content); [button setTarget:web_view]; [button setAction:@selector(goBack:)]; button = MakeTestButton(&button_rect, @"Forward", content); [button setTarget:web_view]; [button setAction:@selector(goForward:)]; // reload button button = MakeTestButton(&button_rect, @"Reload", content); [button setTarget:web_view]; [button setAction:@selector(reload:)]; // stop button button = MakeTestButton(&button_rect, @"Stop", content); [button setTarget:web_view]; [button setAction:@selector(stopLoading:)]; // text field for URL button_rect.origin.x += BUTTON_MARGIN; button_rect.size.width = [[m_mainWnd contentView] bounds].size.width - button_rect.origin.x - BUTTON_MARGIN; m_editWnd = [[NSTextField alloc] initWithFrame:button_rect]; [[m_mainWnd contentView] addSubview:m_editWnd]; [m_editWnd setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)]; [m_editWnd setTarget:web_view]; [m_editWnd setAction:@selector(takeURLStringValueFrom:)]; [[m_editWnd cell] setWraps:NO]; [[m_editWnd cell] setScrollable:YES]; // show the window [m_mainWnd makeKeyAndOrderFront: nil]; // Load our initial content. if (starting_url.is_valid()) LoadURL(starting_url); if (IsSVGTestURL(starting_url)) { SizeTo(kSVGTestWindowWidth, kSVGTestWindowHeight); } else { SizeToDefault(); } return true; } void TestShell::TestFinished() { if (!test_is_pending_) return; // reached when running under test_shell_tests test_is_pending_ = false; MessageLoop::current()->Quit(); } // A class to be the target/selector of the "watchdog" thread that ensures // pages timeout if they take too long and tells the test harness via stdout. @interface WatchDogTarget : NSObject { @private NSTimeInterval timeout_; } // |timeout| is in seconds - (id)initWithTimeout:(NSTimeInterval)timeout; // serves as the "run" method of a NSThread. - (void)run:(id)sender; @end @implementation WatchDogTarget - (id)initWithTimeout:(NSTimeInterval)timeout { if ((self = [super init])) { timeout_ = timeout; } return self; } - (void)run:(id)ignore { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // check for debugger, just bail if so. We don't want the timeouts hitting // when we're trying to track down an issue. if (base::debug::BeingDebugged()) return; NSThread* currentThread = [NSThread currentThread]; // Wait to be cancelled. If we are that means the test finished. If it hasn't, // then we need to tell the layout script we timed out and start again. NSDate* limitDate = [NSDate dateWithTimeIntervalSinceNow:timeout_]; while ([(NSDate*)[NSDate date] compare:limitDate] == NSOrderedAscending && ![currentThread isCancelled]) { // sleep for a small increment then check again NSDate* incrementDate = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:incrementDate]; } if (![currentThread isCancelled]) { // Print a warning to be caught by the layout-test script. // Note: the layout test driver may or may not recognize // this as a timeout. puts("#TEST_TIMED_OUT\n"); puts("#EOF\n"); fflush(stdout); abort(); } [pool release]; } @end void TestShell::WaitTestFinished() { DCHECK(!test_is_pending_) << "cannot be used recursively"; test_is_pending_ = true; // Create a watchdog thread which just sets a timer and // kills the process if it times out. This catches really // bad hangs where the shell isn't coming back to the // message loop. If the watchdog is what catches a // timeout, it can't do anything except terminate the test // shell, which is unfortunate. // Windows multiplies by 2.5, but that causes us to run for far, far too // long. We use the passed value and let the scripts flag override // the value as needed. NSTimeInterval timeout_seconds = GetLayoutTestTimeoutForWatchDog() / 1000; WatchDogTarget* watchdog = [[[WatchDogTarget alloc] initWithTimeout:timeout_seconds] autorelease]; NSThread* thread = [[NSThread alloc] initWithTarget:watchdog selector:@selector(run:) object:nil]; [thread start]; // TestFinished() will post a quit message to break this loop when the page // finishes loading. while (test_is_pending_) MessageLoop::current()->Run(); // Tell the watchdog that we're finished. No point waiting to re-join, it'll // die on its own. [thread cancel]; [thread release]; } void TestShell::InteractiveSetFocus(WebWidgetHost* host, bool enable) { if (enable) { [[host->view_handle() window] makeKeyAndOrderFront:nil]; } else { // There is no way to resign key window status in Cocoa. Fake it by // ordering the window out (transferring key status to another window) and // then ordering the window back in, but without making it key. [[host->view_handle() window] orderOut:nil]; [[host->view_handle() window] orderFront:nil]; } } // static void TestShell::DestroyWindow(gfx::NativeWindow windowHandle) { // This code is like -cleanup: on our window delegate. This call needs to be // able to force down a window for tests, so it closes down the window making // sure it cleans up the window delegate and the test shells list of windows // and map of windows to shells. TestShell::RemoveWindowFromList(windowHandle); TestShell::DestroyAssociatedShell(windowHandle); id windowDelegate = [windowHandle delegate]; DCHECK(windowDelegate); [windowHandle setDelegate:nil]; [windowDelegate release]; [windowHandle close]; [windowHandle autorelease]; } WebWidget* TestShell::CreatePopupWidget() { DCHECK(!m_popupHost); m_popupHost = WebWidgetHost::Create(webViewWnd(), popup_delegate_.get()); return m_popupHost->webwidget(); } void TestShell::ClosePopup() { // PostMessage(popupWnd(), WM_CLOSE, 0, 0); m_popupHost = NULL; } void TestShell::SizeTo(int width, int height) { // WebViewHost::Create() sets the HTML content rect to start 32 pixels below // the top of the window to account for the "toolbar". We need to match that // here otherwise the HTML content area will be too short. NSRect r = [m_mainWnd contentRectForFrameRect:[m_mainWnd frame]]; r.size.width = width; r.size.height = height + URLBAR_HEIGHT; [m_mainWnd setFrame:[m_mainWnd frameRectForContentRect:r] display:YES]; } void TestShell::ResizeSubViews() { // handled by Cocoa for us } /* static */ void TestShell::DumpAllBackForwardLists(string16* result) { result->clear(); for (WindowList::iterator iter = TestShell::windowList()->begin(); iter != TestShell::windowList()->end(); iter++) { NSWindow* window = *iter; WindowMap::iterator it = window_map_.Get().find(window); if (it != window_map_.Get().end()) it->second->DumpBackForwardList(result); else LOG(ERROR) << "Failed to find shell for window during dump"; } } void TestShell::LoadURLForFrame(const GURL& url, const std::wstring& frame_name) { if (!url.is_valid()) return; if (IsSVGTestURL(url)) { SizeTo(kSVGTestWindowWidth, kSVGTestWindowHeight); } else { // only resize back to the default when running tests if (layout_test_mode()) SizeToDefault(); } navigation_controller_->LoadEntry( new TestNavigationEntry(-1, url, std::wstring(), frame_name)); } bool TestShell::PromptForSaveFile(const wchar_t* prompt_title, FilePath* result) { NSSavePanel* save_panel = [NSSavePanel savePanel]; /* set up new attributes */ [save_panel setRequiredFileType:@"txt"]; [save_panel setMessage: [NSString stringWithUTF8String:WideToUTF8(prompt_title).c_str()]]; /* display the NSSavePanel */ if ([save_panel runModalForDirectory:NSHomeDirectory() file:@""] == NSOKButton) { *result = FilePath([[save_panel filename] fileSystemRepresentation]); return true; } return false; } // static std::string TestShell::RewriteLocalUrl(const std::string& url) { // Convert file:///tmp/LayoutTests urls to the actual location on disk. const char kPrefix[] = "file:///tmp/LayoutTests/"; const int kPrefixLen = arraysize(kPrefix) - 1; std::string new_url(url); if (url.compare(0, kPrefixLen, kPrefix, kPrefixLen) == 0) { FilePath replace_path; PathService::Get(base::DIR_SOURCE_ROOT, &replace_path); replace_path = replace_path.Append( "third_party/WebKit/LayoutTests/"); new_url = std::string("file://") + replace_path.value() + url.substr(kPrefixLen); } return new_url; } // static void TestShell::ShowStartupDebuggingDialog() { NSAlert* alert = [[[NSAlert alloc] init] autorelease]; alert.messageText = @"Attach to me?"; alert.informativeText = @"This would probably be a good time to attach your " "debugger."; [alert addButtonWithTitle:@"OK"]; [alert runModal]; } base::StringPiece TestShell::ResourceProvider(int key) { base::StringPiece res; g_resource_data_pack->GetStringPiece(key, &res); return res; } //----------------------------------------------------------------------------- namespace webkit_glue { string16 GetLocalizedString(int message_id) { base::StringPiece res; if (!g_resource_data_pack->GetStringPiece(message_id, &res)) { LOG(FATAL) << "failed to load webkit string with id " << message_id; } return string16(reinterpret_cast(res.data()), res.length() / 2); } base::StringPiece GetDataResource(int resource_id) { switch (resource_id) { case IDR_BROKENIMAGE: { // Use webkit's broken image icon (16x16) static std::string broken_image_data; if (broken_image_data.empty()) { FilePath path = GetResourcesFilePath(); // In order to match WebKit's colors for the missing image, we have to // use a PNG. The GIF doesn't have the color range needed to correctly // match the TIFF they use in Safari. path = path.AppendASCII("missingImage.png"); bool success = file_util::ReadFileToString(path, &broken_image_data); if (!success) { LOG(FATAL) << "Failed reading: " << path.value(); } } return broken_image_data; } case IDR_TEXTAREA_RESIZER: { // Use webkit's text area resizer image. static std::string resize_corner_data; if (resize_corner_data.empty()) { FilePath path = GetResourcesFilePath(); path = path.AppendASCII("textAreaResizeCorner.png"); bool success = file_util::ReadFileToString(path, &resize_corner_data); if (!success) { LOG(FATAL) << "Failed reading: " << path.value(); } } return resize_corner_data; } case IDR_SEARCH_CANCEL: case IDR_SEARCH_CANCEL_PRESSED: case IDR_SEARCH_MAGNIFIER: case IDR_SEARCH_MAGNIFIER_RESULTS: case IDR_MEDIA_PAUSE_BUTTON: case IDR_MEDIA_PLAY_BUTTON: case IDR_MEDIA_PLAY_BUTTON_DISABLED: case IDR_MEDIA_SOUND_FULL_BUTTON: case IDR_MEDIA_SOUND_NONE_BUTTON: case IDR_MEDIA_SOUND_DISABLED: case IDR_MEDIA_SLIDER_THUMB: case IDR_MEDIA_VOLUME_SLIDER_THUMB: case IDR_INPUT_SPEECH: case IDR_INPUT_SPEECH_RECORDING: case IDR_INPUT_SPEECH_WAITING: return TestShell::ResourceProvider(resource_id); default: break; } return base::StringPiece(); } bool DownloadUrl(const std::string& url, NSWindow* caller_window) { return false; } void DidLoadPlugin(const std::string& filename) { } void DidUnloadPlugin(const std::string& filename) { } } // namespace webkit_glue