blob: 835ff4fba0bd2aa271090f4a42c1e9d279f5ecd1 [file] [log] [blame]
// 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 <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>
#import <objc/objc-runtime.h>
#include <sys/stat.h>
#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/bundle_locations.h"
#include "base/mac/cocoa_protocols.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_nsautorelease_pool.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_shell_webkit_init.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 <std::map<gfx::NativeWindow, TestShell *> >
TestShell::window_map_ = LAZY_INSTANCE_INITIALIZER;
// 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<NSWindowDelegate> {
@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<TestShellWebView*>(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::FrameworkBundle() 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::FrameworkBundle() 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 <app bundle's parent dir>/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<TestShellWebView*>(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 {
base::mac::ScopedNSAutoreleasePool scoped_pool;
// 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();
}
}
@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 string16& 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, 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;
}
//-----------------------------------------------------------------------------
string16 TestShellWebKitInit::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;
}
// Data packs hold strings as either UTF8 or UTF16.
string16 msg;
switch (g_resource_data_pack->GetTextEncodingType()) {
case ui::DataPack::UTF8:
msg = UTF8ToUTF16(res);
break;
case ui::DataPack::UTF16:
msg = string16(reinterpret_cast<const char16*>(res.data()),
res.length() / 2);
break;
case ui::DataPack::BINARY:
NOTREACHED();
break;
}
return msg;
}
base::StringPiece TestShellWebKitInit::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();
}
namespace webkit_glue {
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